| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439 |
- 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})
|