Initial commit: EFINTEL ERP Django Port
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
from django.contrib import admin
|
||||
from .models import Project, Category, Expense, UserProfile, AuditLog, SystemSetting, Document
|
||||
|
||||
@admin.register(Project)
|
||||
class ProjectAdmin(admin.ModelAdmin):
|
||||
list_display = ('code', 'name', 'manager', 'type', 'actual_cost_clp')
|
||||
search_fields = ('code', 'name')
|
||||
|
||||
@admin.register(Category)
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('code', 'name', 'parent')
|
||||
search_fields = ('code', 'name')
|
||||
|
||||
@admin.register(Expense)
|
||||
class ExpenseAdmin(admin.ModelAdmin):
|
||||
list_display = ('expense_code', 'date', 'project', 'amount_clp', 'approval_status', 'user')
|
||||
list_filter = ('project', 'approval_status', 'currency', 'date')
|
||||
search_fields = ('expense_code', 'description', 'vendor')
|
||||
|
||||
@admin.register(UserProfile)
|
||||
class UserProfileAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'rut', 'job_title')
|
||||
|
||||
@admin.register(AuditLog)
|
||||
class AuditLogAdmin(admin.ModelAdmin):
|
||||
list_display = ('timestamp', 'user', 'action', 'entity_name', 'entity_id')
|
||||
list_filter = ('action', 'entity_name', 'timestamp')
|
||||
|
||||
@admin.register(SystemSetting)
|
||||
class SystemSettingAdmin(admin.ModelAdmin):
|
||||
list_display = ('global_budget', 'usd_exchange_rate')
|
||||
|
||||
@admin.register(Document)
|
||||
class DocumentAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'expense', 'uploaded_by', 'uploaded_at')
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ExpensesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'expenses'
|
||||
|
||||
def ready(self):
|
||||
import expenses.signals
|
||||
@@ -0,0 +1,29 @@
|
||||
import json
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
|
||||
class ExpenseConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
# Todos los clientes se conectan a este grupo para escuchar actualizaciones de gastos
|
||||
self.group_name = 'expenses_updates'
|
||||
await self.channel_layer.group_add(
|
||||
self.group_name,
|
||||
self.channel_name
|
||||
)
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
await self.channel_layer.group_discard(
|
||||
self.group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
# Recibir actualización desde el Channel Layer
|
||||
async def expense_update(self, event):
|
||||
expense_data = event['expense']
|
||||
action = event['action']
|
||||
|
||||
# Enviar actualización por WebSocket al cliente
|
||||
await self.send(text_data=json.dumps({
|
||||
'action': action,
|
||||
'expense': expense_data
|
||||
}))
|
||||
@@ -0,0 +1,19 @@
|
||||
from functools import wraps
|
||||
from django.shortcuts import redirect
|
||||
from django.contrib import messages
|
||||
|
||||
|
||||
def role_required(*group_names):
|
||||
"""Decorator that checks if the user belongs to at least one of the specified groups."""
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
if request.user.is_superuser:
|
||||
return view_func(request, *args, **kwargs)
|
||||
user_groups = request.user.groups.values_list('name', flat=True)
|
||||
if any(group in user_groups for group in group_names):
|
||||
return view_func(request, *args, **kwargs)
|
||||
messages.error(request, 'No tienes permisos para acceder a esta sección.')
|
||||
return redirect('dashboard')
|
||||
return wrapper
|
||||
return decorator
|
||||
@@ -0,0 +1,133 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User, Group
|
||||
from .models import Expense, Project, Category, SystemSetting, UserProfile, Document, CurrencyEnum, ExpenseTypeEnum
|
||||
|
||||
|
||||
class ExpenseForm(forms.ModelForm):
|
||||
date = forms.DateField(
|
||||
widget=forms.DateInput(attrs={'type': 'date'}),
|
||||
label='Fecha del Gasto'
|
||||
)
|
||||
|
||||
amount_clp = forms.DecimalField(
|
||||
label='Monto Neto (CLP)',
|
||||
required=True,
|
||||
widget=forms.NumberInput(attrs={'placeholder': 'Ej: 1450000'})
|
||||
)
|
||||
|
||||
receipt = forms.FileField(
|
||||
label='Adjuntar Boleta/Factura',
|
||||
required=False,
|
||||
widget=forms.FileInput(attrs={'accept': 'image/*,.pdf'})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Expense
|
||||
fields = [
|
||||
'project', 'category', 'amount_clp', 'currency', 'date',
|
||||
'description', 'vendor', 'region', 'comuna', 'vehicle_ownership'
|
||||
]
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'rows': 3, 'placeholder': 'Ej: Carga combustible Copec / Pasajes bus terreno'}),
|
||||
}
|
||||
|
||||
|
||||
class ProjectForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ['code', 'name', 'type', 'description', 'manager', 'assigned_budget_clp']
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
|
||||
class SystemSettingForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = SystemSetting
|
||||
fields = ['global_budget', 'usd_exchange_rate']
|
||||
|
||||
|
||||
class ResponsableForm(forms.Form):
|
||||
username = forms.CharField(max_length=150, label="Nombre de Usuario")
|
||||
password = forms.CharField(widget=forms.PasswordInput(), label="Contraseña", required=False)
|
||||
email = forms.EmailField(label="Correo Electrónico", required=False)
|
||||
role = forms.ModelChoiceField(queryset=Group.objects.all(), label="Rol / Grupo")
|
||||
|
||||
# Perfil Fields
|
||||
rut = forms.CharField(max_length=20, label="RUT", required=False)
|
||||
job_title = forms.CharField(max_length=150, label="Cargo", required=False)
|
||||
description = forms.CharField(widget=forms.Textarea(attrs={'rows': 3}), label="Descripción / Bio", required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance = kwargs.pop('instance', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance:
|
||||
self.fields['username'].initial = self.instance.username
|
||||
self.fields['email'].initial = self.instance.email
|
||||
# Set role
|
||||
groups = self.instance.groups.all()
|
||||
if groups.exists():
|
||||
self.fields['role'].initial = groups.first()
|
||||
# Set profile info
|
||||
try:
|
||||
self.fields['description'].initial = self.instance.profile.description
|
||||
self.fields['rut'].initial = self.instance.profile.rut
|
||||
self.fields['job_title'].initial = self.instance.profile.job_title
|
||||
except Exception:
|
||||
pass
|
||||
# Password is optional for edit
|
||||
self.fields['password'].required = False
|
||||
self.fields['password'].help_text = "Dejar en blanco si no desea cambiarla."
|
||||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data.get('username')
|
||||
qs = User.objects.filter(username=username)
|
||||
if self.instance:
|
||||
qs = qs.exclude(pk=self.instance.pk)
|
||||
if qs.exists():
|
||||
raise forms.ValidationError("Este nombre de usuario ya está registrado.")
|
||||
return username
|
||||
|
||||
def save(self):
|
||||
username = self.cleaned_data.get('username')
|
||||
password = self.cleaned_data.get('password')
|
||||
email = self.cleaned_data.get('email')
|
||||
role = self.cleaned_data.get('role')
|
||||
description = self.cleaned_data.get('description')
|
||||
rut = self.cleaned_data.get('rut')
|
||||
job_title = self.cleaned_data.get('job_title')
|
||||
|
||||
if self.instance:
|
||||
user = self.instance
|
||||
user.username = username
|
||||
user.email = email
|
||||
if password:
|
||||
user.set_password(password)
|
||||
else:
|
||||
user = User.objects.create_user(username=username, email=email, password=password)
|
||||
|
||||
if role.name == 'Administrador' or role.name == 'Administrador Finanzas':
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
else:
|
||||
user.is_staff = False
|
||||
user.is_superuser = False
|
||||
|
||||
user.save()
|
||||
|
||||
user.groups.clear()
|
||||
user.groups.add(role)
|
||||
|
||||
profile, _ = UserProfile.objects.get_or_create(user=user)
|
||||
profile.description = description
|
||||
profile.rut = rut
|
||||
profile.job_title = job_title
|
||||
profile.save()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class DocumentForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Document
|
||||
fields = ['file', 'title']
|
||||
@@ -0,0 +1,155 @@
|
||||
# Generated by Django 6.1 on 2026-08-14 17:39
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SystemSetting',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('global_budget', models.DecimalField(decimal_places=2, default=25000000.0, max_digits=15, verbose_name='Presupuesto General I+D+i')),
|
||||
('usd_exchange_rate', models.DecimalField(decimal_places=2, default=950.0, max_digits=10, verbose_name='Tasa USD/CLP por Defecto')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Configuración del Sistema',
|
||||
'verbose_name_plural': 'Configuraciones del Sistema',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AuditLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(max_length=100, verbose_name='Acción')),
|
||||
('entity_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Entidad (Tabla)')),
|
||||
('entity_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='ID Entidad')),
|
||||
('field_changed', models.CharField(blank=True, max_length=100, null=True, verbose_name='Campo Modificado')),
|
||||
('old_value', models.TextField(blank=True, null=True, verbose_name='Valor Anterior')),
|
||||
('new_value', models.TextField(blank=True, null=True, verbose_name='Valor Nuevo')),
|
||||
('description', models.TextField(verbose_name='Detalle del Cambio')),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Fecha y Hora')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Usuario')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Log de Auditoría (SAP CDHDR/CDPOS)',
|
||||
'verbose_name_plural': 'Logs de Auditoría',
|
||||
'ordering': ['-timestamp'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=10, unique=True, verbose_name='Código de Categoría')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Nombre de Categoría')),
|
||||
('description', models.TextField(blank=True, verbose_name='Descripción')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subcategories', to='expenses.category')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Categoría',
|
||||
'verbose_name_plural': 'Categorías',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Expense',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('expense_code', models.CharField(default=uuid.uuid4, max_length=50, unique=True, verbose_name='Código Transacción')),
|
||||
('amount_clp', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Monto Neto (CLP)')),
|
||||
('amount_usd', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Monto Neto (USD)')),
|
||||
('currency', models.CharField(choices=[('CLP', 'Pesos Chilenos'), ('USD', 'Dólares Estadounidenses')], default='CLP', max_length=10, verbose_name='Moneda Original')),
|
||||
('exchange_rate', models.DecimalField(decimal_places=2, default=950.0, max_digits=10, verbose_name='Tipo de Cambio')),
|
||||
('description', models.TextField(verbose_name='Detalle de Imputación')),
|
||||
('date', models.DateField(verbose_name='Fecha del Gasto')),
|
||||
('vendor', models.CharField(blank=True, max_length=200, null=True, verbose_name='Proveedor / Emisor')),
|
||||
('region', models.CharField(blank=True, max_length=100, null=True, verbose_name='Región (Chile)')),
|
||||
('comuna', models.CharField(blank=True, max_length=100, null=True, verbose_name='Comuna')),
|
||||
('vehicle_ownership', models.CharField(blank=True, max_length=100, null=True, verbose_name='Modalidad / Vehículo')),
|
||||
('approval_status', models.CharField(choices=[('EN_REVISION_DOCUMENTAL', 'En Revisión Documental (Paso 1)'), ('SOLICITADO_FINANZAS', 'Solicitado a Finanzas (Paso 2)'), ('LIBERADO_PAGADO', 'Pago Liberado (Paso 3)'), ('OBSERVADO_RECHAZADO', 'Observado / Rechazado')], default='EN_REVISION_DOCUMENTAL', max_length=50, verbose_name='Estado de Aprobación')),
|
||||
('rejection_reason', models.TextField(blank=True, null=True, verbose_name='Motivo de Observación/Rechazo')),
|
||||
('paid_at', models.DateTimeField(blank=True, null=True, verbose_name='Fecha de Pago')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='expenses', to='expenses.category', verbose_name='Subcategoría')),
|
||||
('elevated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='expenses_elevated', to=settings.AUTH_USER_MODEL, verbose_name='Elevado por (Paso 2)')),
|
||||
('paid_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='expenses_paid', to=settings.AUTH_USER_MODEL, verbose_name='Pagado por (Paso 3)')),
|
||||
('responsible_person', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='expenses_responsible', to=settings.AUTH_USER_MODEL, verbose_name='Responsable Solicitante')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expenses_created', to=settings.AUTH_USER_MODEL, verbose_name='Registrado por')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Gasto',
|
||||
'verbose_name_plural': 'Gastos',
|
||||
'ordering': ['-date', '-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Document',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(upload_to='evidences/', verbose_name='Archivo Adjunto')),
|
||||
('file_type', models.CharField(blank=True, max_length=50, verbose_name='Tipo de Archivo')),
|
||||
('title', models.CharField(blank=True, max_length=200, verbose_name='Título del Documento')),
|
||||
('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='Fecha de Subida')),
|
||||
('uploaded_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Subido por')),
|
||||
('expense', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='expenses.expense', verbose_name='Gasto Relacionado')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Documento Evidencia',
|
||||
'verbose_name_plural': 'Documentos Evidencias',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Project',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(default='PROY-000', max_length=50, unique=True, verbose_name='Código')),
|
||||
('name', models.CharField(max_length=200, verbose_name='Nombre del Proyecto/Centro')),
|
||||
('description', models.TextField(blank=True, verbose_name='Descripción')),
|
||||
('assigned_budget_clp', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Presupuesto Asignado (CLP)')),
|
||||
('assigned_budget_usd', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Presupuesto Asignado (USD)')),
|
||||
('actual_cost_clp', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Costo Real Acumulado (CLP)')),
|
||||
('type', models.CharField(choices=[('COSTO_DIRECTO_PROYECTO', 'Costo Directo Proyecto'), ('GASTO_OPERACIONAL_OVERHEAD', 'Gasto Operacional Overhead')], default='COSTO_DIRECTO_PROYECTO', max_length=50, verbose_name='Tipo de Centro')),
|
||||
('status', models.CharField(default='on-budget', max_length=50, verbose_name='Estado de Presupuesto')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Fecha de Creación')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Última Modificación')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_projects', to=settings.AUTH_USER_MODEL, verbose_name='Creado por')),
|
||||
('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_projects', to=settings.AUTH_USER_MODEL, verbose_name='Encargado')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Proyecto / Centro de Costo',
|
||||
'verbose_name_plural': 'Proyectos y Centros',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='expense',
|
||||
name='project',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expenses', to='expenses.project', verbose_name='Proyecto'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('rut', models.CharField(blank=True, max_length=20, null=True, verbose_name='RUT')),
|
||||
('job_title', models.CharField(blank=True, max_length=150, null=True, verbose_name='Cargo')),
|
||||
('description', models.TextField(blank=True, verbose_name='Descripción del Responsable')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Fecha de Registro')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Última Modificación')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='Usuario')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Perfil de Usuario',
|
||||
'verbose_name_plural': 'Perfiles de Usuario',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,163 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
import uuid
|
||||
|
||||
# Enums
|
||||
class CurrencyEnum(models.TextChoices):
|
||||
CLP = 'CLP', 'Pesos Chilenos'
|
||||
USD = 'USD', 'Dólares Estadounidenses'
|
||||
|
||||
class ExpenseTypeEnum(models.TextChoices):
|
||||
COSTO_DIRECTO = 'COSTO_DIRECTO_PROYECTO', 'Costo Directo Proyecto'
|
||||
GASTO_OPERACIONAL = 'GASTO_OPERACIONAL_OVERHEAD', 'Gasto Operacional Overhead'
|
||||
|
||||
class ApprovalStatusEnum(models.TextChoices):
|
||||
EN_REVISION = 'EN_REVISION_DOCUMENTAL', 'En Revisión Documental (Paso 1)'
|
||||
SOLICITADO_FINANZAS = 'SOLICITADO_FINANZAS', 'Solicitado a Finanzas (Paso 2)'
|
||||
LIBERADO_PAGADO = 'LIBERADO_PAGADO', 'Pago Liberado (Paso 3)'
|
||||
OBSERVADO_RECHAZADO = 'OBSERVADO_RECHAZADO', 'Observado / Rechazado'
|
||||
|
||||
class Project(models.Model):
|
||||
# Added core project/costcenter fields mapped from Prisma
|
||||
code = models.CharField(max_length=50, unique=True, verbose_name="Código", default='PROY-000')
|
||||
name = models.CharField(max_length=200, verbose_name="Nombre del Proyecto/Centro")
|
||||
description = models.TextField(blank=True, verbose_name="Descripción")
|
||||
|
||||
manager = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='managed_projects', verbose_name="Encargado")
|
||||
|
||||
assigned_budget_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Presupuesto Asignado (CLP)")
|
||||
assigned_budget_usd = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Presupuesto Asignado (USD)")
|
||||
actual_cost_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Costo Real Acumulado (CLP)")
|
||||
|
||||
type = models.CharField(max_length=50, choices=ExpenseTypeEnum.choices, default=ExpenseTypeEnum.COSTO_DIRECTO, verbose_name="Tipo de Centro")
|
||||
status = models.CharField(max_length=50, default="on-budget", verbose_name="Estado de Presupuesto")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Fecha de Creación")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Última Modificación")
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_projects', verbose_name="Creado por")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code} - {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Proyecto / Centro de Costo"
|
||||
verbose_name_plural = "Proyectos y Centros"
|
||||
|
||||
class Category(models.Model):
|
||||
# Category / Subcategory hierarchy
|
||||
code = models.CharField(max_length=10, unique=True, verbose_name="Código de Categoría")
|
||||
name = models.CharField(max_length=100, verbose_name="Nombre de Categoría")
|
||||
description = models.TextField(blank=True, verbose_name="Descripción")
|
||||
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='subcategories')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code}: {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Categoría"
|
||||
verbose_name_plural = "Categorías"
|
||||
|
||||
class Expense(models.Model):
|
||||
expense_code = models.CharField(max_length=50, unique=True, default=uuid.uuid4, verbose_name="Código Transacción")
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='expenses', verbose_name="Proyecto")
|
||||
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, related_name='expenses', verbose_name="Subcategoría")
|
||||
|
||||
# Financial fields
|
||||
amount_clp = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="Monto Neto (CLP)")
|
||||
amount_usd = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="Monto Neto (USD)")
|
||||
currency = models.CharField(max_length=10, choices=CurrencyEnum.choices, default=CurrencyEnum.CLP, verbose_name="Moneda Original")
|
||||
exchange_rate = models.DecimalField(max_digits=10, decimal_places=2, default=950.0, verbose_name="Tipo de Cambio")
|
||||
|
||||
description = models.TextField(verbose_name="Detalle de Imputación")
|
||||
date = models.DateField(verbose_name="Fecha del Gasto")
|
||||
|
||||
vendor = models.CharField(max_length=200, blank=True, null=True, verbose_name="Proveedor / Emisor")
|
||||
|
||||
# Geographic & Logistic fields (Operaciones en terreno)
|
||||
region = models.CharField(max_length=100, blank=True, null=True, verbose_name="Región (Chile)")
|
||||
comuna = models.CharField(max_length=100, blank=True, null=True, verbose_name="Comuna")
|
||||
vehicle_ownership = models.CharField(max_length=100, blank=True, null=True, verbose_name="Modalidad / Vehículo")
|
||||
|
||||
# 3-step Approval Flow Tracking
|
||||
approval_status = models.CharField(max_length=50, choices=ApprovalStatusEnum.choices, default=ApprovalStatusEnum.EN_REVISION, verbose_name="Estado de Aprobación")
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expenses_created', verbose_name="Registrado por")
|
||||
responsible_person = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='expenses_responsible', verbose_name="Responsable Solicitante")
|
||||
elevated_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='expenses_elevated', verbose_name="Elevado por (Paso 2)")
|
||||
paid_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='expenses_paid', verbose_name="Pagado por (Paso 3)")
|
||||
|
||||
rejection_reason = models.TextField(blank=True, null=True, verbose_name="Motivo de Observación/Rechazo")
|
||||
paid_at = models.DateTimeField(null=True, blank=True, verbose_name="Fecha de Pago")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.expense_code} - {self.amount_clp} CLP ({self.date})"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Gasto"
|
||||
verbose_name_plural = "Gastos"
|
||||
ordering = ['-date', '-created_at']
|
||||
|
||||
class Document(models.Model):
|
||||
expense = models.ForeignKey(Expense, on_delete=models.CASCADE, related_name='documents', verbose_name="Gasto Relacionado")
|
||||
file = models.FileField(upload_to='evidences/', verbose_name="Archivo Adjunto")
|
||||
file_type = models.CharField(max_length=50, blank=True, verbose_name="Tipo de Archivo")
|
||||
title = models.CharField(max_length=200, blank=True, verbose_name="Título del Documento")
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name="Fecha de Subida")
|
||||
uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="Subido por")
|
||||
|
||||
def __str__(self):
|
||||
return f"Evidencia para {self.expense.expense_code}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Documento Evidencia"
|
||||
verbose_name_plural = "Documentos Evidencias"
|
||||
|
||||
class UserProfile(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile', verbose_name="Usuario")
|
||||
rut = models.CharField(max_length=20, blank=True, null=True, verbose_name="RUT")
|
||||
job_title = models.CharField(max_length=150, blank=True, null=True, verbose_name="Cargo")
|
||||
description = models.TextField(blank=True, verbose_name="Descripción del Responsable")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Fecha de Registro")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Última Modificación")
|
||||
|
||||
def __str__(self):
|
||||
return f"Perfil de {self.user.username}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Perfil de Usuario"
|
||||
verbose_name_plural = "Perfiles de Usuario"
|
||||
|
||||
class AuditLog(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Usuario")
|
||||
action = models.CharField(max_length=100, verbose_name="Acción")
|
||||
entity_name = models.CharField(max_length=100, blank=True, null=True, verbose_name="Entidad (Tabla)")
|
||||
entity_id = models.CharField(max_length=100, blank=True, null=True, verbose_name="ID Entidad")
|
||||
field_changed = models.CharField(max_length=100, blank=True, null=True, verbose_name="Campo Modificado")
|
||||
old_value = models.TextField(blank=True, null=True, verbose_name="Valor Anterior")
|
||||
new_value = models.TextField(blank=True, null=True, verbose_name="Valor Nuevo")
|
||||
|
||||
description = models.TextField(verbose_name="Detalle del Cambio")
|
||||
timestamp = models.DateTimeField(auto_now_add=True, verbose_name="Fecha y Hora")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.timestamp} - {self.user.username if self.user else 'Sistema'}: {self.action}"
|
||||
|
||||
class Meta:
|
||||
ordering = ['-timestamp']
|
||||
verbose_name = "Log de Auditoría (SAP CDHDR/CDPOS)"
|
||||
verbose_name_plural = "Logs de Auditoría"
|
||||
|
||||
class SystemSetting(models.Model):
|
||||
global_budget = models.DecimalField(max_digits=15, decimal_places=2, default=25000000.00, verbose_name="Presupuesto General I+D+i")
|
||||
usd_exchange_rate = models.DecimalField(max_digits=10, decimal_places=2, default=950.0, verbose_name="Tasa USD/CLP por Defecto")
|
||||
|
||||
def __str__(self):
|
||||
return "Configuración Global EFINTEL"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Configuración del Sistema"
|
||||
verbose_name_plural = "Configuraciones del Sistema"
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/expenses/$', consumers.ExpenseConsumer.as_asgi()),
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from channels.layers import get_channel_layer
|
||||
from asgiref.sync import async_to_sync
|
||||
from .models import Expense
|
||||
|
||||
@receiver(post_save, sender=Expense)
|
||||
def expense_saved(sender, instance, created, **kwargs):
|
||||
channel_layer = get_channel_layer()
|
||||
action = 'created' if created else 'updated'
|
||||
|
||||
expense_data = {
|
||||
'id': instance.id,
|
||||
'amount': str(instance.amount),
|
||||
'category': instance.category.name if instance.category else None,
|
||||
'project': instance.project.name if instance.project else None,
|
||||
'user': instance.user.username if instance.user else None,
|
||||
'date': instance.date.strftime('%Y-%m-%d'),
|
||||
'description': instance.description,
|
||||
}
|
||||
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
'expenses_updates',
|
||||
{
|
||||
'type': 'expense_update',
|
||||
'action': action,
|
||||
'expense': expense_data
|
||||
}
|
||||
)
|
||||
|
||||
@receiver(post_delete, sender=Expense)
|
||||
def expense_deleted(sender, instance, **kwargs):
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
'expenses_updates',
|
||||
{
|
||||
'type': 'expense_update',
|
||||
'action': 'deleted',
|
||||
'expense': {'id': instance.id}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,32 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.login_view, name='login'),
|
||||
path('logout/', views.logout_view, name='logout'),
|
||||
|
||||
path('', views.dashboard, name='dashboard'),
|
||||
path('gastos/', views.expense_list, name='expense_list'),
|
||||
path('gastos/nuevo/', views.expense_create, name='expense_create'),
|
||||
path('gastos/<int:pk>/editar/', views.expense_edit, name='expense_edit'),
|
||||
path('gastos/<int:pk>/eliminar/', views.expense_delete, name='expense_delete'),
|
||||
|
||||
# Flujo de aprobación
|
||||
path('gastos/<int:pk>/aprobar/<str:action>/', views.approve_expense, name='approve_expense'),
|
||||
|
||||
# Export / Import
|
||||
path('gastos/exportar/csv/', views.export_expenses_csv, name='export_csv'),
|
||||
|
||||
path('proyectos/', views.project_list, name='project_list'),
|
||||
|
||||
path('gestion/', views.gestion_panel, name='gestion_panel'),
|
||||
path('gestion/configuracion/', views.settings_edit, name='settings_edit'),
|
||||
|
||||
path('gestion/proyectos/nuevo/', views.project_create, name='project_create'),
|
||||
path('gestion/proyectos/<int:pk>/editar/', views.project_edit, name='project_edit'),
|
||||
path('gestion/proyectos/<int:pk>/eliminar/', views.project_delete, name='project_delete'),
|
||||
|
||||
path('gestion/responsables/nuevo/', views.responsable_create, name='responsable_create'),
|
||||
path('gestion/responsables/<int:pk>/editar/', views.responsable_edit, name='responsable_edit'),
|
||||
path('gestion/responsables/<int:pk>/eliminar/', views.responsable_delete, name='responsable_delete'),
|
||||
]
|
||||
@@ -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