diff --git a/inventory-backend/app/api/v1/inbound/base.py b/inventory-backend/app/api/v1/inbound/base.py
index 818e6fb..5a5d53a 100644
--- a/inventory-backend/app/api/v1/inbound/base.py
+++ b/inventory-backend/app/api/v1/inbound/base.py
@@ -95,7 +95,9 @@ def get_list():
'company': request.args.get('company', ''),
'category': request.args.get('category', ''),
'type': request.args.get('type', ''),
- 'isEnabled': request.args.get('isEnabled', None)
+ 'isEnabled': request.args.get('isEnabled', None),
+ 'orderByColumn': request.args.get('orderByColumn', ''),
+ 'isAsc': request.args.get('isAsc', None)
}
result = MaterialBaseService.get_list(page, limit, filters)
@@ -139,8 +141,11 @@ def export_data():
'isEnabled': request.args.get('isEnabled', None)
}
- # 生成 Excel 文件流
- file_stream = MaterialBaseService.export_excel(filters)
+ # 获取当前用户权限
+ user_permissions = get_current_user_permissions()
+
+ # 生成 Excel 文件流(传入用户权限进行脱敏)
+ file_stream = MaterialBaseService.export_excel(filters, user_permissions)
# 生成文件名:库存统计+年月日+时分秒 (北京时间 UTC+8)
# 简单处理:UTC时间 + 8小时
diff --git a/inventory-backend/app/services/inbound/base_service.py b/inventory-backend/app/services/inbound/base_service.py
index bbfb7d7..6917cb1 100644
--- a/inventory-backend/app/services/inbound/base_service.py
+++ b/inventory-backend/app/services/inbound/base_service.py
@@ -113,8 +113,43 @@ class MaterialBaseService:
"""
获取基础信息列表 (带分页和筛选)
"""
+ from sqlalchemy import func
try:
- query = MaterialBase.query
+ # 构建聚合子查询
+ buy_sub = db.session.query(
+ StockBuy.base_id,
+ func.sum(StockBuy.stock_quantity).label('buy_inv'),
+ func.sum(StockBuy.available_quantity).label('buy_avail')
+ ).group_by(StockBuy.base_id).subquery()
+
+ semi_sub = db.session.query(
+ StockSemi.base_id,
+ func.sum(StockSemi.stock_quantity).label('semi_inv'),
+ func.sum(StockSemi.available_quantity).label('semi_avail')
+ ).group_by(StockSemi.base_id).subquery()
+
+ prod_sub = db.session.query(
+ StockProduct.base_id,
+ func.sum(StockProduct.stock_quantity).label('prod_inv'),
+ func.sum(StockProduct.available_quantity).label('prod_avail')
+ ).group_by(StockProduct.base_id).subquery()
+
+ # 总库存和可用数的 SQL 表达式
+ total_inv = func.coalesce(buy_sub.c.buy_inv, 0) + \
+ func.coalesce(semi_sub.c.semi_inv, 0) + \
+ func.coalesce(prod_sub.c.prod_inv, 0)
+ total_avail = func.coalesce(buy_sub.c.buy_avail, 0) + \
+ func.coalesce(semi_sub.c.semi_avail, 0) + \
+ func.coalesce(prod_sub.c.prod_avail, 0)
+
+ # 主查询,关联聚合子查询
+ query = db.session.query(
+ MaterialBase,
+ total_inv.label('total_inv'),
+ total_avail.label('total_avail')
+ ).outerjoin(buy_sub, MaterialBase.id == buy_sub.c.base_id)\
+ .outerjoin(semi_sub, MaterialBase.id == semi_sub.c.base_id)\
+ .outerjoin(prod_sub, MaterialBase.id == prod_sub.c.base_id)
if filters:
# 1. 关键词模糊搜索
@@ -127,7 +162,6 @@ class MaterialBaseService:
))
# 2. 精确筛选
- # 公司筛选
if filters.get('company'):
query = query.filter_by(company_name=filters['company'])
@@ -141,23 +175,31 @@ class MaterialBaseService:
is_active = bool(int(filters['isEnabled']))
query = query.filter_by(is_enabled=is_active)
- # [修改3] 默认排序方式改为按 spec_model 排序
- pagination = query.order_by(MaterialBase.spec_model.asc()).paginate(page=page, per_page=limit,
- error_out=False)
+ # 排序处理
+ order_by_column = filters.get('orderByColumn', '')
+ is_asc = filters.get('isAsc', None)
+ if order_by_column == 'inventoryCount':
+ if is_asc == 'asc':
+ query = query.order_by(total_inv.asc())
+ else:
+ query = query.order_by(total_inv.desc())
+ elif order_by_column == 'availableCount':
+ if is_asc == 'asc':
+ query = query.order_by(total_avail.asc())
+ else:
+ query = query.order_by(total_avail.desc())
+ else:
+ # 默认按规格型号升序
+ query = query.order_by(MaterialBase.spec_model.asc())
+
+ # 分页
+ pagination = query.paginate(page=page, per_page=limit, error_out=False)
items_list = []
- for item in pagination.items:
+ for item, inv, avail in pagination.items:
item_dict = item.to_dict()
-
- # 聚合库存
- buy_inv, buy_avail = MaterialBaseService._get_stock_counts(item.stock_buys)
- semi_inv, semi_avail = MaterialBaseService._get_stock_counts(item.stock_semis)
- prod_inv, prod_avail = MaterialBaseService._get_stock_counts(item.stock_products)
- serv_inv, serv_avail = MaterialBaseService._get_stock_counts(getattr(item, 'stock_services', []))
-
- item_dict['inventoryCount'] = buy_inv + semi_inv + prod_inv + serv_inv
- item_dict['availableCount'] = buy_avail + semi_avail + prod_avail + serv_avail
-
+ item_dict['inventoryCount'] = float(inv) if inv is not None else 0.0
+ item_dict['availableCount'] = float(avail) if avail is not None else 0.0
items_list.append(item_dict)
return {"total": pagination.total, "items": items_list}
@@ -307,13 +349,14 @@ class MaterialBaseService:
raise e
# ==============================================================================
- # [核心修改] 统一资产统计导出
+ # [核心修改] 统一资产统计导出(增加用户权限脱敏)
# ==============================================================================
@staticmethod
- def export_excel(filters=None):
+ def export_excel(filters=None, user_permissions=None):
"""
全口径资产统计报表:
逻辑:先查库存表 (Buy, Semi, Product),关联基础信息,只导出实际存在的库存记录。
+ 并根据用户权限对字段进行脱敏。
"""
try:
# 1. 构造基础信息的筛选条件 (用于过滤库存)
@@ -390,7 +433,7 @@ class MaterialBaseService:
"qty": qty,
"avail": float(stock.available_quantity or 0),
"price_excl": unit_price,
- "total_val_excl": total_val_excl, # [新增]
+ "total_val_excl": total_val_excl,
"tax": tax_rate,
"price_incl": price_incl,
"total_val": total_val_incl
@@ -418,7 +461,7 @@ class MaterialBaseService:
"qty": qty,
"avail": float(stock.available_quantity or 0),
"price_excl": cost,
- "total_val_excl": total_val_excl, # [新增]
+ "total_val_excl": total_val_excl,
"tax": 0.0,
"price_incl": cost,
"total_val": total_val_incl
@@ -444,7 +487,7 @@ class MaterialBaseService:
"qty": qty,
"avail": float(stock.available_quantity or 0),
"price_excl": cost,
- "total_val_excl": total_val_excl, # [新增]
+ "total_val_excl": total_val_excl,
"tax": 0.0,
"price_incl": cost,
"total_val": total_val_incl
@@ -463,7 +506,7 @@ class MaterialBaseService:
ws = wb.active
ws.title = "库存统计"
- # 表头 [修改] 增加 "资产总额 (不含税)"
+ # 表头
headers = [
"所属公司", "资产名称", "规格型号", "物料类型",
"类别一级", "类别二级", "类别三级", "类别四级", "类别五级",
@@ -475,6 +518,26 @@ class MaterialBaseService:
]
ws.append(headers)
+ # 确定各字段在表头中的列索引
+ col_idx = {}
+ for idx, header in enumerate(headers):
+ if header == "所属公司":
+ col_idx['companyName'] = idx
+ elif header == "资产名称":
+ col_idx['name'] = idx
+ elif header == "规格型号":
+ col_idx['spec'] = idx
+ elif header == "物料类型":
+ col_idx['type'] = idx
+ elif header in ("类别一级","类别二级","类别三级","类别四级","类别五级"):
+ col_idx.setdefault('category_cols', []).append(idx)
+ elif header == "计量单位":
+ col_idx['unit'] = idx
+ elif header == "库存数量":
+ col_idx['inventoryCount'] = idx
+ elif header == "可用数量":
+ col_idx['availableCount'] = idx
+
# 样式
header_fill = PatternFill(start_color="D7E4BC", end_color="D7E4BC", fill_type="solid")
border_style = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'),
@@ -486,7 +549,19 @@ class MaterialBaseService:
cell.fill = header_fill
cell.border = border_style
- # 写入数据
+ # 字段到权限码的映射
+ field_to_perm = {
+ 'companyName': 'material_list:companyName',
+ 'name': 'material_list:name',
+ 'spec': 'material_list:spec',
+ 'type': 'material_list:type',
+ 'unit': 'material_list:unit',
+ 'category': 'material_list:category',
+ 'inventoryCount': 'material_list:inventoryCount',
+ 'availableCount': 'material_list:availableCount'
+ }
+
+ # 写入数据,并脱敏
for r in all_rows:
base = r['base']
# 类别拆分
@@ -512,11 +587,22 @@ class MaterialBaseService:
r['qty'],
r['avail'],
r['price_excl'],
- r['total_val_excl'], # [新增] 对应列
+ r['total_val_excl'],
r['tax'],
r['price_incl'],
r['total_val']
]
+
+ # 根据用户权限脱敏
+ if user_permissions and 'material_list:*' not in user_permissions:
+ for field, perm_code in field_to_perm.items():
+ if perm_code not in user_permissions:
+ if field == 'category':
+ for cat_idx in col_idx.get('category_cols', []):
+ row_val[cat_idx] = ''
+ elif field in col_idx:
+ row_val[col_idx[field]] = ''
+
ws.append(row_val)
# 列宽调整
diff --git a/inventory-web/src/views/material/list.vue b/inventory-web/src/views/material/list.vue
index d6c0c99..5bc6dac 100644
--- a/inventory-web/src/views/material/list.vue
+++ b/inventory-web/src/views/material/list.vue
@@ -121,6 +121,7 @@
border
stripe
:size="tableSize"
+ @sort-change="handleSortChange"
style="width: 100%; margin-top: 15px"
>
@@ -149,7 +150,7 @@
-
+
{{ row.inventoryCount }}
@@ -157,7 +158,7 @@
-
+
{{ row.availableCount }}
@@ -463,6 +464,8 @@ interface QueryParams {
type: string;
company: string;
isEnabled?: number;
+ orderByColumn: string;
+ isAsc: string | undefined;
}
interface CascaderOption {
@@ -566,7 +569,9 @@ const queryParams = reactive({
category: '',
type: '',
company: '',
- isEnabled: undefined
+ isEnabled: undefined,
+ orderByColumn: '',
+ isAsc: undefined
});
// --- 弹窗与表单相关 ---
@@ -754,6 +759,18 @@ const handleInputSearch = () => {
}, 500);
};
+const handleSortChange = ({ column, prop, order }: any) => {
+ if (prop && (prop === 'inventoryCount' || prop === 'availableCount')) {
+ queryParams.orderByColumn = prop;
+ queryParams.isAsc = order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : undefined;
+ } else {
+ queryParams.orderByColumn = '';
+ queryParams.isAsc = undefined;
+ }
+ queryParams.pageNum = 1;
+ getList();
+};
+
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
@@ -765,6 +782,8 @@ const resetQuery = () => {
queryParams.type = '';
queryParams.company = '';
queryParams.isEnabled = undefined;
+ queryParams.orderByColumn = '';
+ queryParams.isAsc = undefined;
handleQuery();
};