import csv import io from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib.auth import login, authenticate, logout from django.contrib import messages from django.db.models import Sum, Count, F from django.utils import timezone from datetime import date from django.http import JsonResponse, HttpResponse from .models import Expense, Project, Category, SystemSetting, UserProfile, AuditLog, ApprovalStatusEnum, CurrencyEnum from .forms import ExpenseForm, ProjectForm, SystemSettingForm, ResponsableForm from .decorators import role_required from django.contrib.auth.models import User, Group 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) return redirect('dashboard') else: messages.error(request, 'Usuario o contraseña incorrectos.') return render(request, 'registration/login.html') def logout_view(request): logout(request) return redirect('login') @login_required def dashboard(request): today = date.today() first_day_of_month = today.replace(day=1) total_month_clp = Expense.objects.filter( date__gte=first_day_of_month ).aggregate(total=Sum('amount_clp'))['total'] or 0 total_all_clp = Expense.objects.aggregate( total=Sum('amount_clp') )['total'] or 0 project_count = Project.objects.count() today_count = Expense.objects.filter(date=today).count() recent_expenses = Expense.objects.select_related( 'project', 'category', 'user', 'responsible_person' ).order_by('-date')[:10] # Presupuesto Global setting, _ = SystemSetting.objects.get_or_create(id=1) global_budget = setting.global_budget remaining_budget = global_budget - total_all_clp context = { 'total_month': total_month_clp, 'total_all': total_all_clp, 'project_count': project_count, 'today_count': today_count, 'recent_expenses': recent_expenses, 'global_budget': global_budget, 'remaining_budget': remaining_budget, } return render(request, 'expenses/dashboard.html', context) @login_required def expense_list(request): expenses = Expense.objects.select_related('project', 'category', 'user').order_by('-date') # Filtros project_id = request.GET.get('project') category_id = request.GET.get('category') if project_id: expenses = expenses.filter(project_id=project_id) if category_id: expenses = expenses.filter(category_id=category_id) context = { 'expenses': expenses, 'projects': Project.objects.all(), 'categories': Category.objects.all(), 'selected_project': project_id, 'selected_category': category_id, } return render(request, 'expenses/expense_list.html', context) @login_required def expense_create(request): # Only allow creation if user is admin or allowed to if request.method == 'POST': form = ExpenseForm(request.POST, request.FILES) if form.is_valid(): expense = form.save(commit=False) expense.user = request.user # Auto convert to USD if CLP setting, _ = SystemSetting.objects.get_or_create(id=1) if expense.currency == CurrencyEnum.CLP: expense.amount_usd = expense.amount_clp / setting.usd_exchange_rate else: expense.amount_usd = expense.amount_clp expense.amount_clp = expense.amount_usd * setting.usd_exchange_rate expense.save() receipt = request.FILES.get('receipt') if receipt: from .models import Document Document.objects.create(expense=expense, file=receipt, title=receipt.name, uploaded_by=request.user) AuditLog.objects.create( user=request.user, action="Gasto Creado", entity_name="Expense", entity_id=expense.expense_code, description=f"Se creó el gasto por {expense.amount_clp} CLP" ) messages.success(request, 'Gasto registrado exitosamente.') return redirect('expense_list') else: form = ExpenseForm() return render(request, 'expenses/expense_form.html', {'form': form}) @login_required def expense_edit(request, pk): expense = get_object_or_404(Expense, pk=pk) user_groups = request.user.groups.values_list('name', flat=True) if not (request.user.is_superuser or 'Administrador' in user_groups or expense.user == request.user): messages.error(request, 'No tienes permisos para editar este gasto.') return redirect('expense_list') old_amount = expense.amount_clp if request.method == 'POST': form = ExpenseForm(request.POST, request.FILES, instance=expense) if form.is_valid(): exp = form.save(commit=False) setting, _ = SystemSetting.objects.get_or_create(id=1) exp.amount_usd = exp.amount_clp / setting.usd_exchange_rate exp.save() receipt = request.FILES.get('receipt') if receipt: from .models import Document # Optionally delete old receipt: Document.objects.filter(expense=exp).delete() Document.objects.create(expense=exp, file=receipt, title=receipt.name, uploaded_by=request.user) AuditLog.objects.create( user=request.user, action="Gasto Editado", entity_name="Expense", entity_id=exp.expense_code, field_changed="amount_clp", old_value=str(old_amount), new_value=str(exp.amount_clp), description=f"Se editó el monto del gasto" ) messages.success(request, 'Gasto actualizado exitosamente.') return redirect('expense_list') else: form = ExpenseForm(instance=expense) return render(request, 'expenses/expense_form.html', {'form': form}) @login_required def expense_delete(request, pk): expense = get_object_or_404(Expense, pk=pk) user_groups = request.user.groups.values_list('name', flat=True) if not (request.user.is_superuser or 'Administrador' in user_groups): messages.error(request, 'Solo administradores pueden eliminar gastos.') return redirect('expense_list') if request.method == 'POST': code = expense.expense_code expense.delete() AuditLog.objects.create( user=request.user, action="Gasto Eliminado", entity_name="Expense", entity_id=code, description="Se eliminó el gasto de la base de datos" ) messages.success(request, 'Gasto eliminado.') return redirect('expense_list') # Flujo de 3 pasos @login_required def approve_expense(request, pk, action): expense = get_object_or_404(Expense, pk=pk) user_groups = request.user.groups.values_list('name', flat=True) is_admin = request.user.is_superuser or 'Administrador' in user_groups is_finanzas = 'Administrador Finanzas' in user_groups or is_admin if action == 'solicitar': # Step 1 -> 2 expense.approval_status = ApprovalStatusEnum.SOLICITADO_FINANZAS expense.elevated_by = request.user expense.save() AuditLog.objects.create( user=request.user, action="ELEVATE_REQUEST", entity_name="Expense", entity_id=expense.expense_code, description="Solicitud elevada a finanzas" ) messages.success(request, "Solicitud elevada a finanzas.") elif action == 'pagar': # Step 2 -> 3 if not is_finanzas: messages.error(request, "Solo Finanzas puede liberar el pago.") return redirect('dashboard') expense.approval_status = ApprovalStatusEnum.LIBERADO_PAGADO expense.paid_by = request.user expense.paid_at = timezone.now() expense.save() # Update Project Cost proj = expense.project proj.actual_cost_clp = proj.actual_cost_clp + expense.amount_clp proj.save() AuditLog.objects.create( user=request.user, action="APPROVE_PAYMENT", entity_name="Expense", entity_id=expense.expense_code, description="Pago liberado" ) messages.success(request, "Pago liberado con éxito.") elif action == 'rechazar': expense.approval_status = ApprovalStatusEnum.OBSERVADO_RECHAZADO expense.save() AuditLog.objects.create( user=request.user, action="REJECT", entity_name="Expense", entity_id=expense.expense_code, description="Gasto observado/rechazado" ) messages.warning(request, "Gasto rechazado.") return redirect(request.META.get('HTTP_REFERER', 'dashboard')) @login_required def export_expenses_csv(request): response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = f'attachment; filename="Gastos_EFINTEL_{date.today()}.csv"' writer = csv.writer(response, delimiter=';') writer.writerow([ 'ID_Gasto', 'Codigo_Transaccion', 'Fecha', 'Proyecto_Codigo', 'Proyecto_Nombre', 'Monto_CLP', 'Monto_USD', 'Moneda_Original', 'Estado_Aprobacion', 'Responsable', 'Detalle', 'Region', 'Comuna', 'Proveedor' ]) expenses = Expense.objects.all().select_related('project', 'responsible_person') for exp in expenses: writer.writerow([ exp.id, exp.expense_code, exp.date, exp.project.code, exp.project.name, exp.amount_clp, exp.amount_usd, exp.currency, exp.get_approval_status_display(), exp.responsible_person.username if exp.responsible_person else '', exp.description, exp.region, exp.comuna, exp.vendor ]) AuditLog.objects.create( user=request.user, action="EXPORT_CSV", description="Se descargó la matriz de gastos en formato CSV." ) return response @login_required def project_list(request): projects = Project.objects.annotate( expense_count=Count('expenses'), total_expenses=Sum('expenses__amount_clp') ).select_related('manager') context = { 'projects': projects, } return render(request, 'expenses/project_list.html', context) @login_required @role_required('Administrador') def gestion_panel(request): projects = Project.objects.all().select_related('manager', 'created_by') responsables = User.objects.all().select_related('profile').prefetch_related('groups') settings_obj, _ = SystemSetting.objects.get_or_create(id=1) logs = AuditLog.objects.all().select_related('user')[:100] context = { 'projects': projects, 'responsables': responsables, 'settings': settings_obj, 'logs': logs, } return render(request, 'expenses/gestion.html', context) @login_required @role_required('Administrador') def settings_edit(request): setting, _ = SystemSetting.objects.get_or_create(id=1) if request.method == 'POST': form = SystemSettingForm(request.POST, instance=setting) if form.is_valid(): form.save() messages.success(request, 'Configuración actualizada.') return redirect('gestion_panel') else: form = SystemSettingForm(instance=setting) return render(request, 'expenses/settings_form.html', {'form': form}) @login_required @role_required('Administrador') def project_create(request): if request.method == 'POST': form = ProjectForm(request.POST) if form.is_valid(): project = form.save(commit=False) project.created_by = request.user project.save() messages.success(request, 'Proyecto creado exitosamente.') return redirect('gestion_panel') else: form = ProjectForm() return render(request, 'expenses/project_form.html', {'form': form}) @login_required @role_required('Administrador') def project_edit(request, pk): project = get_object_or_404(Project, pk=pk) if request.method == 'POST': form = ProjectForm(request.POST, instance=project) if form.is_valid(): form.save() messages.success(request, 'Proyecto actualizado exitosamente.') return redirect('gestion_panel') else: form = ProjectForm(instance=project) return render(request, 'expenses/project_form.html', {'form': form}) @login_required @role_required('Administrador') def project_delete(request, pk): project = get_object_or_404(Project, pk=pk) if request.method == 'POST': project.delete() messages.success(request, 'Proyecto eliminado.') return redirect('gestion_panel') @login_required @role_required('Administrador') def responsable_create(request): if request.method == 'POST': form = ResponsableForm(request.POST) if form.is_valid(): form.save() messages.success(request, 'Responsable creado exitosamente.') return redirect('gestion_panel') else: form = ResponsableForm() return render(request, 'expenses/responsable_form.html', {'form': form}) @login_required @role_required('Administrador') def responsable_edit(request, pk): user = get_object_or_404(User, pk=pk) if request.method == 'POST': form = ResponsableForm(request.POST, instance=user) if form.is_valid(): form.save() messages.success(request, 'Responsable actualizado exitosamente.') return redirect('gestion_panel') else: form = ResponsableForm(instance=user) return render(request, 'expenses/responsable_form.html', {'form': form, 'edit_mode': True, 'target_user': user}) @login_required @role_required('Administrador') def responsable_delete(request, pk): user = get_object_or_404(User, pk=pk) if request.method == 'POST': user.delete() messages.success(request, 'Responsable eliminado.') return redirect('gestion_panel')