Files
erp-efintel-2026-portainer/expenses/views.py
T

595 lines
21 KiB
Python

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
import json
from .models import Expense, Project, Category, SystemSetting, UserProfile, AuditLog, ApprovalStatusEnum, CurrencyEnum, Vendor
from .forms import ExpenseForm, ProjectForm, SystemSettingForm, ResponsableForm, CategoryForm, VendorForm
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
# Chart Data: Gastos por Categoría
cat_expenses = Expense.objects.values('category__name').annotate(total=Sum('amount_clp')).order_by('-total')
category_labels = [item['category__name'] or 'Sin Categoría' for item in cat_expenses]
category_data = [float(item['total'] or 0) for item in cat_expenses]
# Chart Data: Gastos por Proyecto
proj_expenses = Expense.objects.values('project__name').annotate(total=Sum('amount_clp')).order_by('-total')
project_labels = [item['project__name'] or 'Sin Proyecto' for item in proj_expenses]
project_data = [float(item['total'] or 0) for item in proj_expenses]
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,
'category_labels_json': json.dumps(category_labels),
'category_data_json': json.dumps(category_data),
'project_labels_json': json.dumps(project_labels),
'project_data_json': json.dumps(project_data),
}
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')
if expense.approval_status in [ApprovalStatusEnum.SOLICITADO_FINANZAS, ApprovalStatusEnum.LIBERADO_PAGADO]:
messages.error(request, 'Gasto bloqueado. No puedes editar un comprobante que ya está en finanzas o pagado.')
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
# Reset status if it was rejected
if exp.approval_status == ApprovalStatusEnum.OBSERVADO_RECHAZADO:
exp.approval_status = ApprovalStatusEnum.EN_REVISION
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 expense.approval_status in [ApprovalStatusEnum.SOLICITADO_FINANZAS, ApprovalStatusEnum.LIBERADO_PAGADO]:
messages.error(request, 'Gasto bloqueado. No puedes eliminar un comprobante que ya está en finanzas o pagado.')
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
if not is_admin and expense.project.manager != request.user:
messages.error(request, "Solo el Administrador o el Encargado del Proyecto pueden solicitar pago a finanzas.")
return redirect('expense_list')
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
if expense.tax_type == TaxTypeEnum.IVA_19:
proj.actual_cost_clp = proj.actual_cost_clp + expense.amount_clp
else:
proj.actual_cost_clp = proj.actual_cost_clp + expense.total_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', 'parent', 'category')
categories = Category.objects.all().select_related('parent')
vendors = Vendor.objects.all()
responsables = User.objects.all().select_related('profile').prefetch_related('groups', 'profile__specialties')
settings_obj, _ = SystemSetting.objects.get_or_create(id=1)
logs = AuditLog.objects.all().select_related('user')[:100]
context = {
'projects': projects,
'categories': categories,
'vendors': vendors,
'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 category_create(request):
if request.method == 'POST':
form = CategoryForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, 'Categoría creada exitosamente.')
return redirect('gestion_panel')
else:
form = CategoryForm()
return render(request, 'expenses/category_form.html', {'form': form})
@login_required
@role_required('Administrador')
def category_edit(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, 'Categoría actualizada exitosamente.')
return redirect('gestion_panel')
else:
form = CategoryForm(instance=category)
return render(request, 'expenses/category_form.html', {'form': form})
@login_required
@role_required('Administrador')
def category_delete(request, pk):
category = get_object_or_404(Category, pk=pk)
if request.method == 'POST':
category.delete()
messages.success(request, 'Categoría eliminada.')
return redirect('gestion_panel')
@login_required
@role_required('Administrador')
def vendor_create(request):
if request.method == 'POST':
form = VendorForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, 'Proveedor creado exitosamente.')
return redirect('gestion_panel')
else:
form = VendorForm()
return render(request, 'expenses/vendor_form.html', {'form': form})
@login_required
@role_required('Administrador')
def vendor_edit(request, pk):
vendor = get_object_or_404(Vendor, pk=pk)
if request.method == 'POST':
form = VendorForm(request.POST, instance=vendor)
if form.is_valid():
form.save()
messages.success(request, 'Proveedor actualizado exitosamente.')
return redirect('gestion_panel')
else:
form = VendorForm(instance=vendor)
return render(request, 'expenses/vendor_form.html', {'form': form})
@login_required
@role_required('Administrador')
def vendor_delete(request, pk):
vendor = get_object_or_404(Vendor, pk=pk)
if request.method == 'POST':
vendor.delete()
messages.success(request, 'Proveedor 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')
from django.db.models import Sum, Count
@login_required
def accounts_payable(request):
user_groups = request.user.groups.values_list('name', flat=True)
is_finanzas = 'Administrador Finanzas' in user_groups or request.user.is_superuser or 'Administrador' in user_groups
if not is_finanzas:
messages.error(request, 'No tienes permisos para ver Cuentas por Pagar.')
return redirect('dashboard')
debts = Expense.objects.filter(approval_status=ApprovalStatusEnum.SOLICITADO_FINANZAS).exclude(vendor__isnull=True).values(
'vendor__id', 'vendor__rut', 'vendor__name', 'vendor__bank', 'vendor__account_type', 'vendor__account_number'
).annotate(
total_clp=Sum('total_amount_clp'),
total_usd=Sum('amount_usd'),
expense_count=Count('id')
).order_by('-total_clp')
return render(request, 'expenses/accounts_payable.html', {'debts': debts})
@login_required
@role_required('Administrador')
def category_create(request):
if request.method == 'POST':
form = CategoryForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, 'Categoría creada exitosamente.')
return redirect('gestion_panel')
else:
form = CategoryForm()
return render(request, 'expenses/category_form.html', {'form': form})
@login_required
@role_required('Administrador')
def category_edit(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, 'Categoría actualizada exitosamente.')
return redirect('gestion_panel')
else:
form = CategoryForm(instance=category)
return render(request, 'expenses/category_form.html', {'form': form})
@login_required
@role_required('Administrador')
def category_delete(request, pk):
category = get_object_or_404(Category, pk=pk)
if request.method == 'POST':
category.delete()
messages.success(request, 'Categoría eliminada.')
return redirect('gestion_panel')