Feat: dashboard con gráficos, estructura CBS/Portafolios, semáforo de proyectos y cartera de especialistas
This commit is contained in:
+178
-5
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user