Feat: dashboard con gráficos, estructura CBS/Portafolios, semáforo de proyectos y cartera de especialistas

This commit is contained in:
Nachou-u
2026-08-20 18:54:58 -04:00
parent 8c0b435555
commit b692614857
25 changed files with 1717 additions and 159 deletions
+178 -5
View File
@@ -8,9 +8,10 @@ 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
from .forms import ExpenseForm, ProjectForm, SystemSettingForm, ResponsableForm
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
@@ -58,6 +59,16 @@ def dashboard(request):
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,
@@ -66,6 +77,10 @@ def dashboard(request):
'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)
@@ -138,6 +153,10 @@ def expense_edit(request, pk):
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':
@@ -146,6 +165,11 @@ def expense_edit(request, pk):
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')
@@ -179,6 +203,10 @@ def expense_delete(request, pk):
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()
@@ -203,6 +231,10 @@ def approve_expense(request, pk, action):
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()
@@ -228,7 +260,10 @@ def approve_expense(request, pk, action):
# Update Project Cost
proj = expense.project
proj.actual_cost_clp = proj.actual_cost_clp + expense.amount_clp
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(
@@ -311,13 +346,17 @@ def project_list(request):
@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')
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,
@@ -381,6 +420,84 @@ def project_delete(request, pk):
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):
@@ -419,3 +536,59 @@ def responsable_delete(request, pk):
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')