from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib.auth import authenticate, login, logout from django.contrib import messages from django.http import HttpResponse, JsonResponse from django.db.models import Count, Q from django.core.paginator import Paginator from django.conf import settings from openpyxl import load_workbook from io import BytesIO from datetime import date, timedelta from .models import Asset, Category, AssetChangeLog from .forms import AssetForm, AssetImportForm, CategoryForm from .excel_utils import ( export_assets_to_excel, generate_import_template, import_assets_from_excel, STATUS_MAP, ) # ─── 认证 ──────────────────────────────────────── def login_view(request): if request.user.is_authenticated: return redirect('dashboard') if request.method == 'POST': username = request.POST.get('username', '') password = request.POST.get('password', '') user = authenticate(request, username=username, password=password) if user is not None: login(request, user) next_url = request.GET.get('next', '/') return redirect(next_url) else: messages.error(request, '用户名或密码错误') return render(request, 'assetapp/login.html') def logout_view(request): logout(request) return redirect('login') # ─── 仪表盘 ────────────────────────────────────── @login_required def dashboard(request): total_assets = Asset.objects.count() status_stats = Asset.objects.values('status').annotate(count=Count('id')) status_data = {item['status']: item['count'] for item in status_stats} category_stats = Asset.objects.values('category__name').annotate( count=Count('id') ).order_by('-count') # 即将过保(30天内) today = date.today() expiring_soon = Asset.objects.filter( warranty_expire__lte=today + timedelta(days=30), warranty_expire__gte=today, status='in_use', ).count() expired = Asset.objects.filter( warranty_expire__lt=today, status='in_use', ).count() # 最近变更 recent_changes = AssetChangeLog.objects.select_related('operator')[:10] context = { 'total_assets': total_assets, 'status_data': status_data, 'status_map': STATUS_MAP, 'category_stats': category_stats, 'expiring_soon': expiring_soon, 'expired': expired, 'recent_changes': recent_changes, } return render(request, 'assetapp/dashboard.html', context) # ─── 资产列表 ───────────────────────────────────── @login_required def asset_list(request): queryset = Asset.objects.select_related('category') # 搜索(模糊搜索所有字段) search = request.GET.get('search', '').strip() if search: queryset = queryset.filter( Q(asset_number__icontains=search) | Q(name__icontains=search) | Q(serial_number__icontains=search) | Q(ip_address__icontains=search) | Q(bmc_address__icontains=search) | Q(brand__icontains=search) | Q(model__icontains=search) | Q(location__icontains=search) | Q(cabinet__icontains=search) | Q(cabinet_position__icontains=search) | Q(responsible_person__icontains=search) | Q(department__icontains=search) | Q(user__icontains=search) | Q(business_type__icontains=search) | Q(gpu_type__icontains=search) | Q(gpu_count__icontains=search) ) # 筛选 category_id = request.GET.get('category') if category_id: queryset = queryset.filter(category_id=category_id) status = request.GET.get('status') if status: queryset = queryset.filter(status=status) cabinet = request.GET.get('cabinet') if cabinet: queryset = queryset.filter(cabinet__icontains=cabinet) business_type = request.GET.get('business_type') if business_type: queryset = queryset.filter(business_type=business_type) # 排序 sort = request.GET.get('sort', 'id') valid_sorts = ['id', '-id', 'asset_number', '-asset_number', 'name', '-name', 'created_at', '-created_at', 'updated_at', '-updated_at', 'purchase_date', '-purchase_date'] if sort not in valid_sorts: sort = '-created_at' queryset = queryset.order_by(sort) # 分页 paginator = Paginator(queryset, settings.ASSETS_PER_PAGE) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) categories = Category.objects.all() cabinets = sorted(set( Asset.objects.values_list('cabinet', flat=True) .exclude(cabinet='') .exclude(cabinet=None) )) business_types = sorted(set( Asset.objects.values_list('business_type', flat=True) .exclude(business_type='') .exclude(business_type=None) )) context = { 'page_obj': page_obj, 'search': search, 'categories': categories, 'current_category': category_id, 'current_status': status, 'current_cabinet': cabinet, 'current_business_type': business_type, 'cabinets': cabinets, 'business_types': business_types, 'status_map': STATUS_MAP, 'sort': sort, } return render(request, 'assetapp/asset_list.html', context) # ─── 资产详情 ───────────────────────────────────── @login_required def asset_detail(request, pk): asset = get_object_or_404(Asset.objects.select_related('category', 'created_by'), pk=pk) change_logs = asset.change_logs.select_related('operator')[:20] context = { 'asset': asset, 'change_logs': change_logs, 'status_map': STATUS_MAP, } return render(request, 'assetapp/asset_detail.html', context) # ─── 资产创建 ───────────────────────────────────── @login_required def asset_create(request): if request.method == 'POST': form = AssetForm(request.POST) if form.is_valid(): asset = form.save(commit=False) asset.created_by = request.user asset.save() AssetChangeLog.objects.create( asset=asset, asset_number=asset.asset_number, action='create', description='创建资产', operator=request.user, ) messages.success(request, f'资产 {asset.asset_number} 创建成功!') return redirect('asset_detail', pk=asset.pk) else: form = AssetForm() context = {'form': form, 'action': 'create'} return render(request, 'assetapp/asset_form.html', context) # ─── 资产编辑 ───────────────────────────────────── @login_required def asset_update(request, pk): asset = get_object_or_404(Asset, pk=pk) if request.method == 'POST': form = AssetForm(request.POST, instance=asset) if form.is_valid(): # 记录变更 changes = [] for field in form.changed_data: old_val = str(form.initial.get(field, '')) new_val = str(form.cleaned_data.get(field, '')) changes.append(f'{field}: {old_val} → {new_val}') AssetChangeLog.objects.create( asset=asset, asset_number=asset.asset_number, action='update', field_name=field, old_value=str(form.initial.get(field, '')), new_value=str(form.cleaned_data.get(field, '')), operator=request.user, ) asset = form.save() if changes: messages.success(request, f'已更新 {len(changes)} 个字段') return redirect('asset_detail', pk=asset.pk) else: form = AssetForm(instance=asset) context = {'form': form, 'action': 'update', 'asset': asset} return render(request, 'assetapp/asset_form.html', context) # ─── 资产删除 ───────────────────────────────────── @login_required def asset_delete(request, pk): asset = get_object_or_404(Asset, pk=pk) if request.method == 'POST': AssetChangeLog.objects.create( asset_number=asset.asset_number, action='delete', description=f'删除资产: {asset.name}', operator=request.user, ) asset.delete() messages.success(request, f'资产 {asset.asset_number} 已删除') return redirect('asset_list') context = {'asset': asset} return render(request, 'assetapp/asset_confirm_delete.html', context) # ─── Excel导出 ──────────────────────────────────── @login_required def asset_export(request): queryset = Asset.objects.select_related('category') # 支持筛选导出 category_id = request.GET.get('category') if category_id: queryset = queryset.filter(category_id=category_id) status = request.GET.get('status') if status: queryset = queryset.filter(status=status) wb = export_assets_to_excel(queryset) output = BytesIO() wb.save(output) output.seek(0) AssetChangeLog.objects.create( asset_number='-', action='export', description=f'导出 {queryset.count()} 条资产记录', operator=request.user, ) today_str = date.today().strftime('%Y%m%d') response = HttpResponse( output.read(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = f'attachment; filename=硬件资产_{today_str}.xlsx' return response # ─── Excel导入 ──────────────────────────────────── @login_required def asset_import(request): if request.method == 'POST': form = AssetImportForm(request.POST, request.FILES) if form.is_valid(): try: wb = load_workbook(request.FILES['excel_file']) ws = wb.active category_map = {c.name: c for c in Category.objects.all()} results = import_assets_from_excel(ws, category_map, operator=request.user) if results['success'] > 0: messages.success(request, f"成功导入 {results['success']} 条资产") if results['skipped'] > 0: messages.warning(request, f"跳过 {results['skipped']} 条(资产编号重复)") if results['errors']: for error in results['errors'][:10]: messages.error(request, error) if len(results['errors']) > 10: messages.error(request, f"...还有 {len(results['errors']) - 10} 条错误") except Exception as e: messages.error(request, f'导入失败: {str(e)}') return redirect('asset_list') else: form = AssetImportForm() return render(request, 'assetapp/asset_import.html', {'form': form}) # ─── 下载导入模板 ────────────────────────────────── @login_required def download_template(request): wb = generate_import_template() output = BytesIO() wb.save(output) output.seek(0) response = HttpResponse( output.read(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = 'attachment; filename=资产导入模板.xlsx' return response # ─── 变更记录 ───────────────────────────────────── @login_required def change_log_list(request): logs = AssetChangeLog.objects.select_related('operator', 'asset') asset_number = request.GET.get('asset_number', '').strip() if asset_number: logs = logs.filter(asset_number__icontains=asset_number) action = request.GET.get('action') if action: logs = logs.filter(action=action) paginator = Paginator(logs, 30) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) context = { 'page_obj': page_obj, 'asset_number': asset_number, 'current_action': action, } return render(request, 'assetapp/changelog.html', context) # ─── 分类管理 ───────────────────────────────────── @login_required def category_list(request): categories = Category.objects.annotate(asset_count=Count('assets')).order_by('name') return render(request, 'assetapp/category_list.html', {'categories': categories}) @login_required def category_create(request): if request.method == 'POST': form = CategoryForm(request.POST) if form.is_valid(): form.save() messages.success(request, f'分类 "{form.instance.name}" 创建成功') return redirect('category_list') else: form = CategoryForm() return render(request, 'assetapp/category_form.html', {'form': form, 'action': 'create'}) @login_required def category_update(request, pk): category = get_object_or_404(Category, pk=pk) if request.method == 'POST': form = CategoryForm(request.POST, instance=category) if form.is_valid(): form.save() messages.success(request, f'分类 "{category.name}" 已更新') return redirect('category_list') else: form = CategoryForm(instance=category) return render(request, 'assetapp/category_form.html', {'form': form, 'action': 'update', 'category': category}) @login_required def category_delete(request, pk): category = get_object_or_404(Category, pk=pk) asset_count = category.assets.count() if asset_count > 0: messages.error(request, f'分类 "{category.name}" 下还有 {asset_count} 个资产,无法删除') return redirect('category_list') if request.method == 'POST': category.delete() messages.success(request, f'分类 "{category.name}" 已删除') return redirect('category_list') return render(request, 'assetapp/category_confirm_delete.html', {'category': category})