Initial commit: EFINTEL ERP Django Port
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user