Compare commits
7 Commits
4223a95f10
...
3bb3975022
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bb3975022 | |||
| 34629b432a | |||
| 990399a408 | |||
| 74089c7d7d | |||
| 6336432a5c | |||
| 1ad57da2a7 | |||
| b375cbfe25 |
@ -157,9 +157,14 @@ def create_app():
|
|||||||
try:
|
try:
|
||||||
from app.services.permission_service import PermissionService
|
from app.services.permission_service import PermissionService
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
# 先清理旧版菜单,再初始化新版
|
||||||
|
PermissionService.cleanup_legacy_stocktake_menus()
|
||||||
PermissionService.init_audit_menu()
|
PermissionService.init_audit_menu()
|
||||||
|
PermissionService.init_stocktake_menus()
|
||||||
|
# 初始化所有菜单的层级结构
|
||||||
|
PermissionService.init_all_menus()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ 审计日志菜单初始化跳过: {e}")
|
print(f"⚠️ 菜单初始化跳过: {e}")
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2.10 注册盘盈盘亏管理模块 (Stock Adjustment)
|
# 2.10 注册盘盈盘亏管理模块 (Stock Adjustment)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import uuid # .material -> .base refactor checked
|
import uuid # .material -> .base refactor checked
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from sqlalchemy import or_, func, desc
|
from sqlalchemy import or_, func, desc, and_
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.outbound import TransOutbound
|
from app.models.outbound import TransOutbound
|
||||||
|
|
||||||
@ -195,25 +195,92 @@ class OutboundService:
|
|||||||
def get_grouped_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None):
|
def get_grouped_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None):
|
||||||
"""
|
"""
|
||||||
查询出库记录(按出库单号分组),包含详细物品信息
|
查询出库记录(按出库单号分组),包含详细物品信息
|
||||||
|
支持跨表搜索:单号、领用人、SKU、物料名称、规格型号
|
||||||
"""
|
"""
|
||||||
# 1. 查询分页单号
|
# 1. 构建基础查询
|
||||||
|
# 如果有关键词,需要联表搜索物料名称和规格型号
|
||||||
|
if keyword:
|
||||||
|
# 由于 TransOutbound 通过 source_table + stock_id 关联到不同库存表,再关联到 MaterialBase
|
||||||
|
# 需要使用 union 或分别查询后合并
|
||||||
|
# 方案:分别查询三种来源的 matching outbound_no,然后合并
|
||||||
|
|
||||||
|
# 查询 stock_buy 路径匹配的名称/规格
|
||||||
|
buy_match = db.session.query(TransOutbound.outbound_no).join(
|
||||||
|
StockBuy, and_(
|
||||||
|
TransOutbound.stock_id == StockBuy.id,
|
||||||
|
TransOutbound.source_table == 'stock_buy'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockBuy.base_id == MaterialBase.id
|
||||||
|
).filter(
|
||||||
|
or_(
|
||||||
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||||
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
# 查询 stock_semi 路径匹配的名称/规格
|
||||||
|
semi_match = db.session.query(TransOutbound.outbound_no).join(
|
||||||
|
StockSemi, and_(
|
||||||
|
TransOutbound.stock_id == StockSemi.id,
|
||||||
|
TransOutbound.source_table == 'stock_semi'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockSemi.base_id == MaterialBase.id
|
||||||
|
).filter(
|
||||||
|
or_(
|
||||||
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||||
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
# 查询 stock_product 路径匹配的名称/规格
|
||||||
|
product_match = db.session.query(TransOutbound.outbound_no).join(
|
||||||
|
StockProduct, and_(
|
||||||
|
TransOutbound.stock_id == StockProduct.id,
|
||||||
|
TransOutbound.source_table == 'stock_product'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockProduct.base_id == MaterialBase.id
|
||||||
|
).filter(
|
||||||
|
or_(
|
||||||
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||||
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
# 合并三种来源的匹配单号
|
||||||
|
all_matches = db.session.query(buy_match.c.outbound_no).union(
|
||||||
|
db.session.query(semi_match.c.outbound_no),
|
||||||
|
db.session.query(product_match.c.outbound_no)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
# 主搜索条件:单号、领用人、SKU + 物料名称/规格匹配的单号
|
||||||
|
keyword_conditions = or_(
|
||||||
|
TransOutbound.outbound_no.ilike(f'%{keyword}%'),
|
||||||
|
TransOutbound.consumer_name.ilike(f'%{keyword}%'),
|
||||||
|
TransOutbound.sku.ilike(f'%{keyword}%'),
|
||||||
|
TransOutbound.outbound_no.in_(all_matches)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
keyword_conditions = None
|
||||||
|
|
||||||
stmt = db.session.query(
|
stmt = db.session.query(
|
||||||
TransOutbound.outbound_no,
|
TransOutbound.outbound_no,
|
||||||
func.max(TransOutbound.outbound_time).label('max_time')
|
func.max(TransOutbound.outbound_time).label('max_time')
|
||||||
).group_by(TransOutbound.outbound_no)
|
).group_by(TransOutbound.outbound_no)
|
||||||
|
|
||||||
if keyword:
|
if keyword_conditions is not None:
|
||||||
stmt = stmt.filter(or_(
|
stmt = stmt.filter(keyword_conditions)
|
||||||
TransOutbound.outbound_no.ilike(f'%{keyword}%'),
|
|
||||||
TransOutbound.consumer_name.ilike(f'%{keyword}%'),
|
|
||||||
TransOutbound.sku.ilike(f'%{keyword}%')
|
|
||||||
))
|
|
||||||
|
|
||||||
if start_date and end_date:
|
if start_date and end_date:
|
||||||
stmt = stmt.filter(TransOutbound.outbound_time.between(start_date, end_date))
|
stmt = stmt.filter(TransOutbound.outbound_time.between(start_date, end_date))
|
||||||
|
|
||||||
stmt = stmt.order_by(desc('max_time'))
|
stmt = stmt.order_by(desc('max_time'))
|
||||||
|
|
||||||
|
# 使用 distinct 确保跨表查询不重复
|
||||||
|
stmt = stmt.distinct()
|
||||||
|
|
||||||
pagination = stmt.paginate(page=page, per_page=per_page, error_out=False)
|
pagination = stmt.paginate(page=page, per_page=per_page, error_out=False)
|
||||||
outbound_nos = [row.outbound_no for row in pagination.items]
|
outbound_nos = [row.outbound_no for row in pagination.items]
|
||||||
|
|
||||||
|
|||||||
@ -159,7 +159,7 @@ class PermissionService:
|
|||||||
# 1. 检查并创建审计日志菜单
|
# 1. 检查并创建审计日志菜单
|
||||||
menu_code = 'system_audit'
|
menu_code = 'system_audit'
|
||||||
existing_menu = SysMenu.query.filter_by(code=menu_code).first()
|
existing_menu = SysMenu.query.filter_by(code=menu_code).first()
|
||||||
|
|
||||||
if not existing_menu:
|
if not existing_menu:
|
||||||
new_menu = SysMenu(
|
new_menu = SysMenu(
|
||||||
parent_id=0,
|
parent_id=0,
|
||||||
@ -201,3 +201,310 @@ class PermissionService:
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
print(f"❌ 初始化审计日志菜单失败: {str(e)}")
|
print(f"❌ 初始化审计日志菜单失败: {str(e)}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def init_stocktake_menus():
|
||||||
|
"""
|
||||||
|
初始化盘点管理菜单和权限
|
||||||
|
包括:顶级菜单、盲盘作业、盈亏调整、以及操作权限元素
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
role_code = 'SUPER_ADMIN'
|
||||||
|
|
||||||
|
# 1. 创建顶级菜单:盘点管理
|
||||||
|
stocktake_mgmt_code = 'stocktake_mgmt'
|
||||||
|
stocktake_menu = SysMenu.query.filter_by(code=stocktake_mgmt_code).first()
|
||||||
|
if not stocktake_menu:
|
||||||
|
stocktake_menu = SysMenu(
|
||||||
|
parent_id=0,
|
||||||
|
name='盘点管理',
|
||||||
|
code=stocktake_mgmt_code,
|
||||||
|
path='/stocktake',
|
||||||
|
sort_order=30,
|
||||||
|
is_visible=True
|
||||||
|
)
|
||||||
|
db.session.add(stocktake_menu)
|
||||||
|
db.session.flush()
|
||||||
|
print(f"✅ 盘点管理顶级菜单已创建")
|
||||||
|
else:
|
||||||
|
print(f"ℹ️ 盘点管理顶级菜单已存在")
|
||||||
|
|
||||||
|
# 2. 创建子菜单:盲盘作业
|
||||||
|
stocktake_op_code = 'inventory_stocktake'
|
||||||
|
stocktake_op_menu = SysMenu.query.filter_by(code=stocktake_op_code).first()
|
||||||
|
if not stocktake_op_menu:
|
||||||
|
stocktake_op_menu = SysMenu(
|
||||||
|
parent_id=stocktake_menu.id,
|
||||||
|
name='盲盘作业',
|
||||||
|
code=stocktake_op_code,
|
||||||
|
path='/stocktake/operation',
|
||||||
|
sort_order=1,
|
||||||
|
is_visible=True
|
||||||
|
)
|
||||||
|
db.session.add(stocktake_op_menu)
|
||||||
|
db.session.flush()
|
||||||
|
print(f"✅ 盲盘作业菜单已创建")
|
||||||
|
else:
|
||||||
|
print(f"ℹ️ 盲盘作业菜单已存在")
|
||||||
|
|
||||||
|
# 3. 为盲盘作业添加操作权限元素
|
||||||
|
stocktake_op_element = SysElement.query.filter_by(
|
||||||
|
menu_code=stocktake_op_code,
|
||||||
|
code='inventory_stocktake:operation'
|
||||||
|
).first()
|
||||||
|
if not stocktake_op_element:
|
||||||
|
stocktake_op_element = SysElement(
|
||||||
|
menu_code=stocktake_op_code,
|
||||||
|
name='盲盘操作',
|
||||||
|
code='inventory_stocktake:operation',
|
||||||
|
element_type='operation'
|
||||||
|
)
|
||||||
|
db.session.add(stocktake_op_element)
|
||||||
|
print(f"✅ 盲盘作业操作权限已创建")
|
||||||
|
else:
|
||||||
|
print(f"ℹ️ 盲盘作业操作权限已存在")
|
||||||
|
|
||||||
|
# 4. 创建子菜单:盈亏调整
|
||||||
|
adjustment_code = 'stock_adjustment'
|
||||||
|
adjustment_menu = SysMenu.query.filter_by(code=adjustment_code).first()
|
||||||
|
if not adjustment_menu:
|
||||||
|
adjustment_menu = SysMenu(
|
||||||
|
parent_id=stocktake_menu.id,
|
||||||
|
name='盈亏调整',
|
||||||
|
code=adjustment_code,
|
||||||
|
path='/stocktake/adjustment',
|
||||||
|
sort_order=2,
|
||||||
|
is_visible=True
|
||||||
|
)
|
||||||
|
db.session.add(adjustment_menu)
|
||||||
|
db.session.flush()
|
||||||
|
print(f"✅ 盈亏调整菜单已创建")
|
||||||
|
else:
|
||||||
|
print(f"ℹ️ 盈亏调整菜单已存在")
|
||||||
|
|
||||||
|
# 5. 为盈亏调整添加列表权限元素 (stock_adjustment:list)
|
||||||
|
adjustment_list_element = SysElement.query.filter_by(
|
||||||
|
menu_code=adjustment_code,
|
||||||
|
code='stock_adjustment:list'
|
||||||
|
).first()
|
||||||
|
if not adjustment_list_element:
|
||||||
|
adjustment_list_element = SysElement(
|
||||||
|
menu_code=adjustment_code,
|
||||||
|
name='盈亏列表',
|
||||||
|
code='stock_adjustment:list',
|
||||||
|
element_type='element'
|
||||||
|
)
|
||||||
|
db.session.add(adjustment_list_element)
|
||||||
|
print(f"✅ 盈亏调整列表权限已创建")
|
||||||
|
else:
|
||||||
|
print(f"ℹ️ 盈亏调整列表权限已存在")
|
||||||
|
|
||||||
|
# 6. 为盈亏调整添加操作权限元素 (stock_adjustment:operation)
|
||||||
|
adjustment_op_element = SysElement.query.filter_by(
|
||||||
|
menu_code=adjustment_code,
|
||||||
|
code='stock_adjustment:operation'
|
||||||
|
).first()
|
||||||
|
if not adjustment_op_element:
|
||||||
|
adjustment_op_element = SysElement(
|
||||||
|
menu_code=adjustment_code,
|
||||||
|
name='盈亏操作',
|
||||||
|
code='stock_adjustment:operation',
|
||||||
|
element_type='operation'
|
||||||
|
)
|
||||||
|
db.session.add(adjustment_op_element)
|
||||||
|
print(f"✅ 盈亏调整操作权限已创建")
|
||||||
|
else:
|
||||||
|
print(f"ℹ️ 盈亏调整操作权限已存在")
|
||||||
|
|
||||||
|
# 7. 为超级管理员分配所有盘点相关权限
|
||||||
|
menu_codes = [stocktake_mgmt_code, stocktake_op_code, adjustment_code]
|
||||||
|
for mc in menu_codes:
|
||||||
|
existing_perm = SysRolePermission.query.filter_by(
|
||||||
|
role_code=role_code,
|
||||||
|
target_code=mc,
|
||||||
|
type='menu'
|
||||||
|
).first()
|
||||||
|
if not existing_perm:
|
||||||
|
new_perm = SysRolePermission(
|
||||||
|
role_code=role_code,
|
||||||
|
target_code=mc,
|
||||||
|
type='menu'
|
||||||
|
)
|
||||||
|
db.session.add(new_perm)
|
||||||
|
print(f"✅ 超级管理员已赋予 {mc} 菜单权限")
|
||||||
|
|
||||||
|
# 8. 分配操作权限
|
||||||
|
op_codes = ['inventory_stocktake:operation', 'stock_adjustment:list', 'stock_adjustment:operation']
|
||||||
|
for oc in op_codes:
|
||||||
|
existing_perm = SysRolePermission.query.filter_by(
|
||||||
|
role_code=role_code,
|
||||||
|
target_code=oc,
|
||||||
|
type='element'
|
||||||
|
).first()
|
||||||
|
if not existing_perm:
|
||||||
|
new_perm = SysRolePermission(
|
||||||
|
role_code=role_code,
|
||||||
|
target_code=oc,
|
||||||
|
type='element'
|
||||||
|
)
|
||||||
|
db.session.add(new_perm)
|
||||||
|
print(f"✅ 超级管理员已赋予 {oc} 操作权限")
|
||||||
|
|
||||||
|
# 9. 提交
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"❌ 初始化盘点管理菜单失败: {str(e)}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cleanup_legacy_stocktake_menus():
|
||||||
|
"""
|
||||||
|
清理残留的旧版库存盘点菜单
|
||||||
|
如果数据库中存在挂在入库管理下的旧版库存盘点菜单,清理掉
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 查找可能存在的旧版库存盘点菜单(入库管理下的 stocktake)
|
||||||
|
# 旧版可能在入库管理 (path like '%inventory%stocktake') 或者 code 包含 stocktake 但不是新的
|
||||||
|
legacy_menus = SysMenu.query.filter(
|
||||||
|
SysMenu.code.in_(['stocktake', 'inventory_stocktake_old'])
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for menu in legacy_menus:
|
||||||
|
# 删除关联的权限
|
||||||
|
SysRolePermission.query.filter_by(target_code=menu.code).delete()
|
||||||
|
# 删除关联的元素
|
||||||
|
SysElement.query.filter_by(menu_code=menu.code).delete()
|
||||||
|
# 删除菜单
|
||||||
|
db.session.delete(menu)
|
||||||
|
print(f"🗑️ 已清理旧版库存盘点菜单: {menu.code} ({menu.name})")
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"⚠️ 清理旧版菜单失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def init_all_menus():
|
||||||
|
"""
|
||||||
|
初始化所有菜单的层级结构,确保权限配置页面显示正确的树形结构
|
||||||
|
按照侧边栏顺序:基础信息 -> 入库 -> 盘点 -> 出库 -> BOM -> 借库 -> 报废 -> 系统
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
role_code = 'SUPER_ADMIN'
|
||||||
|
|
||||||
|
# 定义菜单结构 (code, name, path, parent_code, sort_order)
|
||||||
|
menu_defs = [
|
||||||
|
# 顶级菜单 (按侧边栏顺序)
|
||||||
|
('material_mgmt', '基础信息管理', '/material', None, 10),
|
||||||
|
('inventory_mgmt', '入库管理', '/inventory', None, 20),
|
||||||
|
('stocktake_mgmt', '盘点管理', '/stocktake', None, 30),
|
||||||
|
('outbound_mgmt', '出库管理', '/outbound', None, 40),
|
||||||
|
('bom_mgmt', 'BOM管理', '/bom', None, 50),
|
||||||
|
('operation_mgmt', '借库管理', '/operation', None, 60),
|
||||||
|
('scrap_mgmt', '报废管理', '/scrap', None, 70),
|
||||||
|
('system_mgmt', '系统管理', '/system', None, 80),
|
||||||
|
|
||||||
|
# 基础信息子菜单
|
||||||
|
('material_base', '基础信息', '/material/index', 'material_mgmt', 1),
|
||||||
|
|
||||||
|
# 入库管理子菜单
|
||||||
|
('inbound_buy', '采购入库', '/inventory/buy', 'inventory_mgmt', 1),
|
||||||
|
('inbound_semi', '半成品入库', '/inventory/semi', 'inventory_mgmt', 2),
|
||||||
|
('inbound_product', '成品入库', '/inventory/product', 'inventory_mgmt', 3),
|
||||||
|
('inbound_service', '服务权益', '/inventory/service', 'inventory_mgmt', 4),
|
||||||
|
('inbound_summary', '入库记录', '/inventory/summary', 'inventory_mgmt', 5),
|
||||||
|
|
||||||
|
# 盘点管理子菜单
|
||||||
|
('inventory_stocktake', '盲盘作业', '/stocktake/operation', 'stocktake_mgmt', 1),
|
||||||
|
('stock_adjustment', '盈亏调整', '/stocktake/adjustment', 'stocktake_mgmt', 2),
|
||||||
|
|
||||||
|
# 出库管理子菜单
|
||||||
|
('outbound_selection', '出库选单', '/outbound/selection', 'outbound_mgmt', 1),
|
||||||
|
('outbound_create', '扫码出库', '/outbound/create', 'outbound_mgmt', 2),
|
||||||
|
('outbound_list', '出库记录', '/outbound/index', 'outbound_mgmt', 3),
|
||||||
|
|
||||||
|
# BOM管理子菜单
|
||||||
|
('bom_manage', 'BOM配方管理', '/bom/manage', 'bom_mgmt', 1),
|
||||||
|
|
||||||
|
# 借库管理子菜单
|
||||||
|
('op_borrow', '借库操作', '/operation/borrow', 'operation_mgmt', 1),
|
||||||
|
('op_return', '归还操作', '/operation/repair', 'operation_mgmt', 2),
|
||||||
|
('op_records', '借还记录', '/operation/records', 'operation_mgmt', 3),
|
||||||
|
|
||||||
|
# 报废管理子菜单
|
||||||
|
('scrap_create', '新建报废', '/scrap/create', 'scrap_mgmt', 1),
|
||||||
|
('scrap_list', '报废记录', '/scrap/index', 'scrap_mgmt', 2),
|
||||||
|
|
||||||
|
# 系统管理子菜单
|
||||||
|
('system_user', '员工账号管理', '/system/user-create', 'system_mgmt', 1),
|
||||||
|
('system_permission', '权限分配', '/system/permission', 'system_mgmt', 2),
|
||||||
|
('system_audit', '审计日志', '/system/audit', 'system_mgmt', 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 第一步:清理根级别的冗余子菜单(这些本应是子节点,但可能之前被错误地创建为根节点)
|
||||||
|
child_codes = [m[0] for m in menu_defs if m[3] is not None] # 所有子菜单的code
|
||||||
|
orphaned_menus = SysMenu.query.filter(
|
||||||
|
SysMenu.code.in_(child_codes),
|
||||||
|
(SysMenu.parent_id == 0) | (SysMenu.parent_id.is_(None))
|
||||||
|
).all()
|
||||||
|
for menu in orphaned_menus:
|
||||||
|
print(f"🗑️ 清理根级别冗余菜单: {menu.code} ({menu.name})")
|
||||||
|
# 删除关联的权限
|
||||||
|
SysRolePermission.query.filter_by(target_code=menu.code).delete()
|
||||||
|
db.session.delete(menu)
|
||||||
|
|
||||||
|
# 创建或更新菜单
|
||||||
|
menu_map = {} # code -> menu obj
|
||||||
|
|
||||||
|
for code, name, path, parent_code, sort_order in menu_defs:
|
||||||
|
menu = SysMenu.query.filter_by(code=code).first()
|
||||||
|
if not menu:
|
||||||
|
menu = SysMenu(code=code, name=name, path=path, sort_order=sort_order, is_visible=True)
|
||||||
|
db.session.add(menu)
|
||||||
|
db.session.flush()
|
||||||
|
print(f"✅ 菜单已创建: {name} ({code})")
|
||||||
|
else:
|
||||||
|
# 更新已有菜单的属性
|
||||||
|
menu.name = name
|
||||||
|
menu.path = path
|
||||||
|
menu.sort_order = sort_order
|
||||||
|
|
||||||
|
menu_map[code] = menu
|
||||||
|
|
||||||
|
# 设置 parent_id
|
||||||
|
for code, name, path, parent_code, sort_order in menu_defs:
|
||||||
|
if parent_code and parent_code in menu_map:
|
||||||
|
menu = menu_map[code]
|
||||||
|
parent = menu_map[parent_code]
|
||||||
|
menu.parent_id = parent.id
|
||||||
|
|
||||||
|
# 为超级管理员分配所有菜单权限
|
||||||
|
for code, name, path, parent_code, sort_order in menu_defs:
|
||||||
|
if parent_code is None: # 只分配顶级菜单
|
||||||
|
existing_perm = SysRolePermission.query.filter_by(
|
||||||
|
role_code=role_code,
|
||||||
|
target_code=code,
|
||||||
|
type='menu'
|
||||||
|
).first()
|
||||||
|
if not existing_perm:
|
||||||
|
new_perm = SysRolePermission(
|
||||||
|
role_code=role_code,
|
||||||
|
target_code=code,
|
||||||
|
type='menu'
|
||||||
|
)
|
||||||
|
db.session.add(new_perm)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"✅ 所有菜单初始化完成")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"❌ 初始化菜单失败: {str(e)}")
|
||||||
|
raise e
|
||||||
|
|||||||
@ -5,7 +5,8 @@ from app.models.transaction import TransBorrow
|
|||||||
from app.models.inbound.buy import StockBuy
|
from app.models.inbound.buy import StockBuy
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
from app.models.inbound.product import StockProduct
|
from app.models.inbound.product import StockProduct
|
||||||
from sqlalchemy import desc, func, nullslast, asc
|
from app.models.base import MaterialBase
|
||||||
|
from sqlalchemy import desc, func, nullslast, asc, or_, and_
|
||||||
|
|
||||||
|
|
||||||
class TransService:
|
class TransService:
|
||||||
@ -189,15 +190,79 @@ class TransService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_records(page=1, limit=10, status='all', keyword=None):
|
def get_records(page=1, limit=10, status='all', keyword=None):
|
||||||
q = TransBorrow.query
|
q = TransBorrow.query
|
||||||
|
|
||||||
|
# 如果有关键词,需要联表搜索物料名称和规格型号
|
||||||
|
if keyword:
|
||||||
|
# TransBorrow 通过 source_table + stock_id 关联到不同库存表,再关联到 MaterialBase
|
||||||
|
# 需要使用 union 或分别查询后合并
|
||||||
|
|
||||||
|
# 查询 stock_buy 路径匹配的名称/规格
|
||||||
|
buy_match = db.session.query(TransBorrow.id).join(
|
||||||
|
StockBuy, and_(
|
||||||
|
TransBorrow.stock_id == StockBuy.id,
|
||||||
|
TransBorrow.source_table == 'stock_buy'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockBuy.base_id == MaterialBase.id
|
||||||
|
).filter(
|
||||||
|
or_(
|
||||||
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||||
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
# 查询 stock_semi 路径匹配的名称/规格
|
||||||
|
semi_match = db.session.query(TransBorrow.id).join(
|
||||||
|
StockSemi, and_(
|
||||||
|
TransBorrow.stock_id == StockSemi.id,
|
||||||
|
TransBorrow.source_table == 'stock_semi'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockSemi.base_id == MaterialBase.id
|
||||||
|
).filter(
|
||||||
|
or_(
|
||||||
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||||
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
# 查询 stock_product 路径匹配的名称/规格
|
||||||
|
product_match = db.session.query(TransBorrow.id).join(
|
||||||
|
StockProduct, and_(
|
||||||
|
TransBorrow.stock_id == StockProduct.id,
|
||||||
|
TransBorrow.source_table == 'stock_product'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockProduct.base_id == MaterialBase.id
|
||||||
|
).filter(
|
||||||
|
or_(
|
||||||
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||||
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
# 合并三种来源的匹配 ID
|
||||||
|
all_matches = db.session.query(buy_match.c.id).union(
|
||||||
|
db.session.query(semi_match.c.id),
|
||||||
|
db.session.query(product_match.c.id)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
# 主搜索条件:借用人、SKU、单号 + 物料名称/规格匹配
|
||||||
|
keyword_conditions = or_(
|
||||||
|
TransBorrow.borrower_name.ilike(f'%{keyword}%'),
|
||||||
|
TransBorrow.sku.ilike(f'%{keyword}%'),
|
||||||
|
TransBorrow.borrow_no.ilike(f'%{keyword}%'),
|
||||||
|
TransBorrow.id.in_(all_matches)
|
||||||
|
)
|
||||||
|
q = q.filter(keyword_conditions)
|
||||||
|
|
||||||
if status == 'borrowed':
|
if status == 'borrowed':
|
||||||
q = q.filter(TransBorrow.is_returned == False)
|
q = q.filter(TransBorrow.is_returned == False)
|
||||||
elif status == 'returned':
|
elif status == 'returned':
|
||||||
q = q.filter(TransBorrow.is_returned == True)
|
q = q.filter(TransBorrow.is_returned == True)
|
||||||
|
|
||||||
if keyword:
|
# 使用 distinct 防止跨表查询产生重复记录
|
||||||
q = q.filter(TransBorrow.borrower_name.ilike(f'%{keyword}%') |
|
q = q.distinct()
|
||||||
TransBorrow.sku.ilike(f'%{keyword}%') |
|
|
||||||
TransBorrow.borrow_no.ilike(f'%{keyword}%'))
|
|
||||||
|
|
||||||
q = q.order_by(nullslast(asc(TransBorrow.expected_return_time)))
|
q = q.order_by(nullslast(asc(TransBorrow.expected_return_time)))
|
||||||
pagination = q.paginate(page=page, per_page=limit, error_out=False)
|
pagination = q.paginate(page=page, per_page=limit, error_out=False)
|
||||||
|
|||||||
@ -3,11 +3,12 @@
|
|||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="listQuery.keyword"
|
v-model="listQuery.keyword"
|
||||||
placeholder="单号/姓名/SKU"
|
placeholder="单号/姓名/SKU/名称/规格"
|
||||||
style="width: 200px;"
|
style="width: 250px;"
|
||||||
class="filter-item"
|
class="filter-item"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="fetchData"
|
@input="debouncedSearch"
|
||||||
|
@clear="handleClearSearch"
|
||||||
/>
|
/>
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="listQuery.dateRange"
|
v-model="listQuery.dateRange"
|
||||||
@ -118,13 +119,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive, onBeforeUnmount } from 'vue'
|
||||||
import { getOutboundList } from '@/api/outbound'
|
import { getOutboundList } from '@/api/outbound'
|
||||||
import { Picture } from '@element-plus/icons-vue'
|
import { Picture } from '@element-plus/icons-vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 防抖定时器
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
// 防抖搜索函数
|
||||||
|
const debouncedSearch = () => {
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer)
|
||||||
|
}
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
listQuery.page = 1
|
||||||
|
fetchData()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空搜索时立即触发查询
|
||||||
|
const handleClearSearch = () => {
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer)
|
||||||
|
}
|
||||||
|
listQuery.page = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
// 列与权限Code的映射关系(数据库中的code)
|
// 列与权限Code的映射关系(数据库中的code)
|
||||||
const permissionMap: Record<string, string> = {
|
const permissionMap: Record<string, string> = {
|
||||||
outbound_no: 'outbound_list:outbound_no',
|
outbound_no: 'outbound_list:outbound_no',
|
||||||
@ -213,6 +237,14 @@ const getTagType = (type: string) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 组件销毁前清理定时器,防止内存泄漏
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer)
|
||||||
|
debounceTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -7,7 +7,14 @@
|
|||||||
<el-radio-button label="returned">已归还</el-radio-button>
|
<el-radio-button label="returned">已归还</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
|
|
||||||
<el-input v-model="keyword" placeholder="搜索借用人/SKU" style="width: 200px" @keyup.enter="fetchData" />
|
<el-input
|
||||||
|
v-model="keyword"
|
||||||
|
placeholder="单号/借用人/SKU/名称/规格"
|
||||||
|
style="width: 250px"
|
||||||
|
clearable
|
||||||
|
@input="debouncedSearch"
|
||||||
|
@clear="handleClearSearch"
|
||||||
|
/>
|
||||||
<el-button type="primary" @click="fetchData">查询</el-button>
|
<el-button type="primary" @click="fetchData">查询</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -104,15 +111,38 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import dayjs from 'dayjs' // 建议使用 dayjs 处理日期,如果没有安装,可以用原生 Date
|
import dayjs from 'dayjs'
|
||||||
import 'dayjs/locale/zh-cn' // 导入中文包
|
import 'dayjs/locale/zh-cn'
|
||||||
dayjs.locale('zh-cn')
|
dayjs.locale('zh-cn')
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 防抖定时器
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
// 防抖搜索函数
|
||||||
|
const debouncedSearch = () => {
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer)
|
||||||
|
}
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
page.value = 1
|
||||||
|
fetchData()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空搜索时立即触发查询
|
||||||
|
const handleClearSearch = () => {
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer)
|
||||||
|
}
|
||||||
|
page.value = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
// 列与权限Code的映射关系(数据库中的code)
|
// 列与权限Code的映射关系(数据库中的code)
|
||||||
const permissionMap: Record<string, string> = {
|
const permissionMap: Record<string, string> = {
|
||||||
borrow_no: 'op_records:borrow_no',
|
borrow_no: 'op_records:borrow_no',
|
||||||
@ -203,6 +233,14 @@ const formatExpectedTime = (timeStr: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(fetchData)
|
onMounted(fetchData)
|
||||||
|
|
||||||
|
// 组件销毁前清理定时器,防止内存泄漏
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer)
|
||||||
|
debounceTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Reference in New Issue
Block a user