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 @@ - + - +