feat: add sorting and export desensitization to material list
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
This commit is contained in:
@ -95,7 +95,9 @@ def get_list():
|
|||||||
'company': request.args.get('company', ''),
|
'company': request.args.get('company', ''),
|
||||||
'category': request.args.get('category', ''),
|
'category': request.args.get('category', ''),
|
||||||
'type': request.args.get('type', ''),
|
'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)
|
result = MaterialBaseService.get_list(page, limit, filters)
|
||||||
@ -139,8 +141,11 @@ def export_data():
|
|||||||
'isEnabled': request.args.get('isEnabled', None)
|
'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)
|
||||||
# 简单处理:UTC时间 + 8小时
|
# 简单处理:UTC时间 + 8小时
|
||||||
|
|||||||
@ -113,8 +113,43 @@ class MaterialBaseService:
|
|||||||
"""
|
"""
|
||||||
获取基础信息列表 (带分页和筛选)
|
获取基础信息列表 (带分页和筛选)
|
||||||
"""
|
"""
|
||||||
|
from sqlalchemy import func
|
||||||
try:
|
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:
|
if filters:
|
||||||
# 1. 关键词模糊搜索
|
# 1. 关键词模糊搜索
|
||||||
@ -127,7 +162,6 @@ class MaterialBaseService:
|
|||||||
))
|
))
|
||||||
|
|
||||||
# 2. 精确筛选
|
# 2. 精确筛选
|
||||||
# 公司筛选
|
|
||||||
if filters.get('company'):
|
if filters.get('company'):
|
||||||
query = query.filter_by(company_name=filters['company'])
|
query = query.filter_by(company_name=filters['company'])
|
||||||
|
|
||||||
@ -141,23 +175,31 @@ class MaterialBaseService:
|
|||||||
is_active = bool(int(filters['isEnabled']))
|
is_active = bool(int(filters['isEnabled']))
|
||||||
query = query.filter_by(is_enabled=is_active)
|
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,
|
order_by_column = filters.get('orderByColumn', '')
|
||||||
error_out=False)
|
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 = []
|
items_list = []
|
||||||
for item in pagination.items:
|
for item, inv, avail in pagination.items:
|
||||||
item_dict = item.to_dict()
|
item_dict = item.to_dict()
|
||||||
|
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
|
||||||
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
|
|
||||||
|
|
||||||
items_list.append(item_dict)
|
items_list.append(item_dict)
|
||||||
|
|
||||||
return {"total": pagination.total, "items": items_list}
|
return {"total": pagination.total, "items": items_list}
|
||||||
@ -307,13 +349,14 @@ class MaterialBaseService:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# [核心修改] 统一资产统计导出
|
# [核心修改] 统一资产统计导出(增加用户权限脱敏)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def export_excel(filters=None):
|
def export_excel(filters=None, user_permissions=None):
|
||||||
"""
|
"""
|
||||||
全口径资产统计报表:
|
全口径资产统计报表:
|
||||||
逻辑:先查库存表 (Buy, Semi, Product),关联基础信息,只导出实际存在的库存记录。
|
逻辑:先查库存表 (Buy, Semi, Product),关联基础信息,只导出实际存在的库存记录。
|
||||||
|
并根据用户权限对字段进行脱敏。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. 构造基础信息的筛选条件 (用于过滤库存)
|
# 1. 构造基础信息的筛选条件 (用于过滤库存)
|
||||||
@ -390,7 +433,7 @@ class MaterialBaseService:
|
|||||||
"qty": qty,
|
"qty": qty,
|
||||||
"avail": float(stock.available_quantity or 0),
|
"avail": float(stock.available_quantity or 0),
|
||||||
"price_excl": unit_price,
|
"price_excl": unit_price,
|
||||||
"total_val_excl": total_val_excl, # [新增]
|
"total_val_excl": total_val_excl,
|
||||||
"tax": tax_rate,
|
"tax": tax_rate,
|
||||||
"price_incl": price_incl,
|
"price_incl": price_incl,
|
||||||
"total_val": total_val_incl
|
"total_val": total_val_incl
|
||||||
@ -418,7 +461,7 @@ class MaterialBaseService:
|
|||||||
"qty": qty,
|
"qty": qty,
|
||||||
"avail": float(stock.available_quantity or 0),
|
"avail": float(stock.available_quantity or 0),
|
||||||
"price_excl": cost,
|
"price_excl": cost,
|
||||||
"total_val_excl": total_val_excl, # [新增]
|
"total_val_excl": total_val_excl,
|
||||||
"tax": 0.0,
|
"tax": 0.0,
|
||||||
"price_incl": cost,
|
"price_incl": cost,
|
||||||
"total_val": total_val_incl
|
"total_val": total_val_incl
|
||||||
@ -444,7 +487,7 @@ class MaterialBaseService:
|
|||||||
"qty": qty,
|
"qty": qty,
|
||||||
"avail": float(stock.available_quantity or 0),
|
"avail": float(stock.available_quantity or 0),
|
||||||
"price_excl": cost,
|
"price_excl": cost,
|
||||||
"total_val_excl": total_val_excl, # [新增]
|
"total_val_excl": total_val_excl,
|
||||||
"tax": 0.0,
|
"tax": 0.0,
|
||||||
"price_incl": cost,
|
"price_incl": cost,
|
||||||
"total_val": total_val_incl
|
"total_val": total_val_incl
|
||||||
@ -463,7 +506,7 @@ class MaterialBaseService:
|
|||||||
ws = wb.active
|
ws = wb.active
|
||||||
ws.title = "库存统计"
|
ws.title = "库存统计"
|
||||||
|
|
||||||
# 表头 [修改] 增加 "资产总额 (不含税)"
|
# 表头
|
||||||
headers = [
|
headers = [
|
||||||
"所属公司", "资产名称", "规格型号", "物料类型",
|
"所属公司", "资产名称", "规格型号", "物料类型",
|
||||||
"类别一级", "类别二级", "类别三级", "类别四级", "类别五级",
|
"类别一级", "类别二级", "类别三级", "类别四级", "类别五级",
|
||||||
@ -475,6 +518,26 @@ class MaterialBaseService:
|
|||||||
]
|
]
|
||||||
ws.append(headers)
|
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")
|
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'),
|
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.fill = header_fill
|
||||||
cell.border = border_style
|
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:
|
for r in all_rows:
|
||||||
base = r['base']
|
base = r['base']
|
||||||
# 类别拆分
|
# 类别拆分
|
||||||
@ -512,11 +587,22 @@ class MaterialBaseService:
|
|||||||
r['qty'],
|
r['qty'],
|
||||||
r['avail'],
|
r['avail'],
|
||||||
r['price_excl'],
|
r['price_excl'],
|
||||||
r['total_val_excl'], # [新增] 对应列
|
r['total_val_excl'],
|
||||||
r['tax'],
|
r['tax'],
|
||||||
r['price_incl'],
|
r['price_incl'],
|
||||||
r['total_val']
|
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)
|
ws.append(row_val)
|
||||||
|
|
||||||
# 列宽调整
|
# 列宽调整
|
||||||
|
|||||||
@ -121,6 +121,7 @@
|
|||||||
border
|
border
|
||||||
stripe
|
stripe
|
||||||
:size="tableSize"
|
:size="tableSize"
|
||||||
|
@sort-change="handleSortChange"
|
||||||
style="width: 100%; margin-top: 15px"
|
style="width: 100%; margin-top: 15px"
|
||||||
>
|
>
|
||||||
<el-table-column v-if="columns.id.visible" prop="id" label="ID" min-width="80" align="center" fixed="left" />
|
<el-table-column v-if="columns.id.visible" prop="id" label="ID" min-width="80" align="center" fixed="left" />
|
||||||
@ -149,7 +150,7 @@
|
|||||||
<el-table-column v-if="columns.spec.visible" prop="spec" label="规格型号" min-width="180" show-overflow-tooltip />
|
<el-table-column v-if="columns.spec.visible" prop="spec" label="规格型号" min-width="180" show-overflow-tooltip />
|
||||||
<el-table-column v-if="columns.unit.visible" prop="unit" label="单位" min-width="80" align="center" />
|
<el-table-column v-if="columns.unit.visible" prop="unit" label="单位" min-width="80" align="center" />
|
||||||
|
|
||||||
<el-table-column v-if="columns.inventory.visible" prop="inventoryCount" label="库存数" min-width="100" align="center">
|
<el-table-column v-if="columns.inventory.visible" prop="inventoryCount" label="库存数" min-width="100" align="center" sortable="custom">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :style="{ fontWeight: 'bold', color: row.inventoryCount > 0 ? '#67C23A' : '#909399' }">
|
<span :style="{ fontWeight: 'bold', color: row.inventoryCount > 0 ? '#67C23A' : '#909399' }">
|
||||||
{{ row.inventoryCount }}
|
{{ row.inventoryCount }}
|
||||||
@ -157,7 +158,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column v-if="columns.available.visible" prop="availableCount" label="可用数" min-width="100" align="center">
|
<el-table-column v-if="columns.available.visible" prop="availableCount" label="可用数" min-width="100" align="center" sortable="custom">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :style="{ fontWeight: 'bold', color: row.availableCount > 0 ? '#409EFF' : '#909399' }">
|
<span :style="{ fontWeight: 'bold', color: row.availableCount > 0 ? '#409EFF' : '#909399' }">
|
||||||
{{ row.availableCount }}
|
{{ row.availableCount }}
|
||||||
@ -463,6 +464,8 @@ interface QueryParams {
|
|||||||
type: string;
|
type: string;
|
||||||
company: string;
|
company: string;
|
||||||
isEnabled?: number;
|
isEnabled?: number;
|
||||||
|
orderByColumn: string;
|
||||||
|
isAsc: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CascaderOption {
|
interface CascaderOption {
|
||||||
@ -566,7 +569,9 @@ const queryParams = reactive<QueryParams>({
|
|||||||
category: '',
|
category: '',
|
||||||
type: '',
|
type: '',
|
||||||
company: '',
|
company: '',
|
||||||
isEnabled: undefined
|
isEnabled: undefined,
|
||||||
|
orderByColumn: '',
|
||||||
|
isAsc: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 弹窗与表单相关 ---
|
// --- 弹窗与表单相关 ---
|
||||||
@ -754,6 +759,18 @@ const handleInputSearch = () => {
|
|||||||
}, 500);
|
}, 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 = () => {
|
const handleQuery = () => {
|
||||||
queryParams.pageNum = 1;
|
queryParams.pageNum = 1;
|
||||||
getList();
|
getList();
|
||||||
@ -765,6 +782,8 @@ const resetQuery = () => {
|
|||||||
queryParams.type = '';
|
queryParams.type = '';
|
||||||
queryParams.company = '';
|
queryParams.company = '';
|
||||||
queryParams.isEnabled = undefined;
|
queryParams.isEnabled = undefined;
|
||||||
|
queryParams.orderByColumn = '';
|
||||||
|
queryParams.isAsc = undefined;
|
||||||
handleQuery();
|
handleQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user