1
0
Ficheiros
asset-management/assetapp/views.py
T

440 linhas
15 KiB
Python

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