Feat: dashboard con gráficos, estructura CBS/Portafolios, semáforo de proyectos y cartera de especialistas
This commit is contained in:
@@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.humanize',
|
||||
'channels',
|
||||
'expenses',
|
||||
]
|
||||
@@ -122,6 +123,8 @@ USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
USE_THOUSAND_SEPARATOR = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/6.1/howto/static-files/
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ class ProjectAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Category)
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('code', 'name', 'parent')
|
||||
list_display = ('code', 'name')
|
||||
search_fields = ('code', 'name')
|
||||
|
||||
@admin.register(Expense)
|
||||
|
||||
+42
-3
@@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User, Group
|
||||
from .models import Expense, Project, Category, SystemSetting, UserProfile, Document, CurrencyEnum, ExpenseTypeEnum
|
||||
from .models import Expense, Project, Category, SystemSetting, UserProfile, Document, CurrencyEnum, ExpenseTypeEnum, Vendor
|
||||
|
||||
|
||||
class ExpenseForm(forms.ModelForm):
|
||||
@@ -21,10 +21,22 @@ class ExpenseForm(forms.ModelForm):
|
||||
widget=forms.FileInput(attrs={'accept': 'image/*,.pdf'})
|
||||
)
|
||||
|
||||
tax_amount_clp = forms.DecimalField(
|
||||
label='Monto Impuesto (CLP)',
|
||||
required=False,
|
||||
widget=forms.NumberInput(attrs={'readonly': 'readonly'})
|
||||
)
|
||||
|
||||
total_amount_clp = forms.DecimalField(
|
||||
label='Monto Total (CLP)',
|
||||
required=False,
|
||||
widget=forms.NumberInput(attrs={'readonly': 'readonly'})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Expense
|
||||
fields = [
|
||||
'project', 'category', 'amount_clp', 'currency', 'date',
|
||||
'project', 'category', 'amount_clp', 'tax_type', 'tax_amount_clp', 'total_amount_clp', 'currency', 'date',
|
||||
'description', 'vendor', 'region', 'comuna', 'vehicle_ownership'
|
||||
]
|
||||
widgets = {
|
||||
@@ -35,12 +47,27 @@ class ExpenseForm(forms.ModelForm):
|
||||
class ProjectForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ['code', 'name', 'type', 'description', 'manager', 'assigned_budget_clp']
|
||||
fields = ['code', 'name', 'parent', 'category', 'status', 'type', 'description', 'manager', 'assigned_budget_clp']
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
|
||||
class CategoryForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ['code', 'name', 'parent', 'description']
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
|
||||
class VendorForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Vendor
|
||||
fields = ['rut', 'name', 'business_line', 'bank', 'account_type', 'account_number', 'email', 'payment_terms']
|
||||
|
||||
|
||||
class SystemSettingForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = SystemSetting
|
||||
@@ -57,6 +84,12 @@ class ResponsableForm(forms.Form):
|
||||
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)
|
||||
specialties = forms.ModelMultipleChoiceField(
|
||||
queryset=Category.objects.all(),
|
||||
required=False,
|
||||
widget=forms.CheckboxSelectMultiple(),
|
||||
label="Especialidades Técnicas (Categorías)"
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance = kwargs.pop('instance', None)
|
||||
@@ -73,6 +106,7 @@ class ResponsableForm(forms.Form):
|
||||
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
|
||||
self.fields['specialties'].initial = self.instance.profile.specialties.all()
|
||||
except Exception:
|
||||
pass
|
||||
# Password is optional for edit
|
||||
@@ -124,6 +158,11 @@ class ResponsableForm(forms.Form):
|
||||
profile.job_title = job_title
|
||||
profile.save()
|
||||
|
||||
# Save specialties
|
||||
specialties = self.cleaned_data.get('specialties')
|
||||
if specialties is not None:
|
||||
profile.specialties.set(specialties)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Generated by Django 6.1 on 2026-08-19 22:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('expenses', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Vendor',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('rut', models.CharField(max_length=15, unique=True, verbose_name='RUT Proveedor')),
|
||||
('name', models.CharField(max_length=200, verbose_name='Razón Social / Nombre')),
|
||||
('business_line', models.CharField(blank=True, max_length=200, null=True, verbose_name='Giro Comercial')),
|
||||
('bank', models.CharField(blank=True, max_length=100, null=True, verbose_name='Banco')),
|
||||
('account_type', models.CharField(blank=True, max_length=50, null=True, verbose_name='Tipo de Cuenta')),
|
||||
('account_number', models.CharField(blank=True, max_length=50, null=True, verbose_name='Número de Cuenta')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Correo de Pago/Facturación')),
|
||||
('payment_terms', models.IntegerField(default=30, verbose_name='Plazo de Pago (Días)')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Proveedor',
|
||||
'verbose_name_plural': 'Proveedores',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='expense',
|
||||
name='tax_amount_clp',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Monto Impuesto (CLP)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='expense',
|
||||
name='tax_type',
|
||||
field=models.CharField(choices=[('EXENTO', 'Exento / No Afecto'), ('IVA_19', 'Afecto a IVA (19%)'), ('HONORARIOS_13_75', 'Boleta de Honorarios (13.75%)')], default='IVA_19', max_length=20, verbose_name='Tipo de Impuesto'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='expense',
|
||||
name='total_amount_clp',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Monto Total (CLP)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='expense',
|
||||
name='amount_usd',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Monto Total (USD)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='expense',
|
||||
name='vendor',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='expenses', to='expenses.vendor', verbose_name='Proveedor / Emisor'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 6.1 on 2026-08-19 23:47
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('expenses', '0002_vendor_expense_tax_amount_clp_expense_tax_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='category',
|
||||
name='parent',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.1 on 2026-08-20 15:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('expenses', '0003_remove_category_parent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='expenses.category', verbose_name='Categoría Padre'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.1 on 2026-08-20 22:30
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('expenses', '0004_category_parent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='expenses.project', verbose_name='Portafolio / Proyecto Padre'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 6.1 on 2026-08-20 22:44
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('expenses', '0005_project_parent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='expenses.category', verbose_name='Categoría del Proyecto'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='specialties',
|
||||
field=models.ManyToManyField(blank=True, related_name='specialists', to='expenses.category', verbose_name='Especialidades / Habilidades'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('ACTIVO', 'Activo'), ('PAUSADA', 'En Pausa'), ('TERMINADO', 'Terminado')], default='ACTIVO', max_length=50, verbose_name='Estado de Proyecto'),
|
||||
),
|
||||
]
|
||||
+45
-5
@@ -7,6 +7,11 @@ class CurrencyEnum(models.TextChoices):
|
||||
CLP = 'CLP', 'Pesos Chilenos'
|
||||
USD = 'USD', 'Dólares Estadounidenses'
|
||||
|
||||
class TaxTypeEnum(models.TextChoices):
|
||||
EXENTO = 'EXENTO', 'Exento / No Afecto'
|
||||
IVA_19 = 'IVA_19', 'Afecto a IVA (19%)'
|
||||
HONORARIOS = 'HONORARIOS_13_75', 'Boleta de Honorarios (13.75%)'
|
||||
|
||||
class ExpenseTypeEnum(models.TextChoices):
|
||||
COSTO_DIRECTO = 'COSTO_DIRECTO_PROYECTO', 'Costo Directo Proyecto'
|
||||
GASTO_OPERACIONAL = 'GASTO_OPERACIONAL_OVERHEAD', 'Gasto Operacional Overhead'
|
||||
@@ -17,6 +22,11 @@ class ApprovalStatusEnum(models.TextChoices):
|
||||
LIBERADO_PAGADO = 'LIBERADO_PAGADO', 'Pago Liberado (Paso 3)'
|
||||
OBSERVADO_RECHAZADO = 'OBSERVADO_RECHAZADO', 'Observado / Rechazado'
|
||||
|
||||
class ProjectStatusEnum(models.TextChoices):
|
||||
ACTIVO = 'ACTIVO', 'Activo'
|
||||
PAUSADA = 'PAUSADA', 'En Pausa'
|
||||
TERMINADO = 'TERMINADO', 'Terminado'
|
||||
|
||||
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')
|
||||
@@ -24,13 +34,15 @@ class Project(models.Model):
|
||||
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")
|
||||
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children', verbose_name="Portafolio / Proyecto Padre")
|
||||
|
||||
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")
|
||||
status = models.CharField(max_length=50, choices=ProjectStatusEnum.choices, default=ProjectStatusEnum.ACTIVO, verbose_name="Estado de Proyecto")
|
||||
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True, blank=True, related_name='projects', verbose_name="Categoría del Proyecto")
|
||||
|
||||
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")
|
||||
@@ -44,11 +56,10 @@ class Project(models.Model):
|
||||
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')
|
||||
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children', verbose_name="Categoría Padre")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code}: {self.name}"
|
||||
@@ -57,6 +68,30 @@ class Category(models.Model):
|
||||
verbose_name = "Categoría"
|
||||
verbose_name_plural = "Categorías"
|
||||
|
||||
class Vendor(models.Model):
|
||||
rut = models.CharField(max_length=15, unique=True, verbose_name="RUT Proveedor")
|
||||
name = models.CharField(max_length=200, verbose_name="Razón Social / Nombre")
|
||||
business_line = models.CharField(max_length=200, blank=True, null=True, verbose_name="Giro Comercial")
|
||||
|
||||
# Banking details
|
||||
bank = models.CharField(max_length=100, blank=True, null=True, verbose_name="Banco")
|
||||
account_type = models.CharField(max_length=50, blank=True, null=True, verbose_name="Tipo de Cuenta")
|
||||
account_number = models.CharField(max_length=50, blank=True, null=True, verbose_name="Número de Cuenta")
|
||||
email = models.EmailField(blank=True, null=True, verbose_name="Correo de Pago/Facturación")
|
||||
|
||||
payment_terms = models.IntegerField(default=30, verbose_name="Plazo de Pago (Días)")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.rut} - {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Proveedor"
|
||||
verbose_name_plural = "Proveedores"
|
||||
ordering = ['name']
|
||||
|
||||
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")
|
||||
@@ -64,14 +99,18 @@ class Expense(models.Model):
|
||||
|
||||
# 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)")
|
||||
tax_type = models.CharField(max_length=20, choices=TaxTypeEnum.choices, default=TaxTypeEnum.IVA_19, verbose_name="Tipo de Impuesto")
|
||||
tax_amount_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Monto Impuesto (CLP)")
|
||||
total_amount_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Monto Total (CLP)")
|
||||
|
||||
amount_usd = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="Monto Total (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")
|
||||
vendor = models.ForeignKey(Vendor, on_delete=models.SET_NULL, null=True, blank=True, related_name='expenses', 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)")
|
||||
@@ -120,6 +159,7 @@ class UserProfile(models.Model):
|
||||
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")
|
||||
specialties = models.ManyToManyField('Category', blank=True, related_name='specialists', verbose_name="Especialidades / Habilidades")
|
||||
|
||||
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")
|
||||
|
||||
+10
-1
@@ -11,7 +11,8 @@ urlpatterns = [
|
||||
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
|
||||
# Flujo de aprobación y Cuentas por Pagar
|
||||
path('gastos/cuentas-por-pagar/', views.accounts_payable, name='accounts_payable'),
|
||||
path('gastos/<int:pk>/aprobar/<str:action>/', views.approve_expense, name='approve_expense'),
|
||||
|
||||
# Export / Import
|
||||
@@ -26,6 +27,14 @@ urlpatterns = [
|
||||
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/categorias/nuevo/', views.category_create, name='category_create'),
|
||||
path('gestion/categorias/<int:pk>/editar/', views.category_edit, name='category_edit'),
|
||||
path('gestion/categorias/<int:pk>/eliminar/', views.category_delete, name='category_delete'),
|
||||
|
||||
path('gestion/proveedores/nuevo/', views.vendor_create, name='vendor_create'),
|
||||
path('gestion/proveedores/<int:pk>/editar/', views.vendor_edit, name='vendor_edit'),
|
||||
path('gestion/proveedores/<int:pk>/eliminar/', views.vendor_delete, name='vendor_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'),
|
||||
|
||||
+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')
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import os
|
||||
import django
|
||||
import sys
|
||||
|
||||
# Set up Django environment
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
django.setup()
|
||||
|
||||
from expenses.models import Category
|
||||
|
||||
def run_seed():
|
||||
print("Clearing existing categories to ensure clean state...")
|
||||
Category.objects.all().delete()
|
||||
|
||||
print("Seeding professional CBS categories...")
|
||||
|
||||
# Define the structure
|
||||
cbs_structure = {
|
||||
"1000": {
|
||||
"name": "Recursos Humanos y Especialidades",
|
||||
"children": {
|
||||
"1100": "Personal Staff Core (Fijo / Sueldos Base)",
|
||||
"1201": "Electricidad de Potencia e Infraestructura",
|
||||
"1202": "Automatización y Control Industrial",
|
||||
"1203": "Domótica e Inmótica (BMS & Climatización)",
|
||||
"1204": "Electrónica y Sistemas de Hardware (PCB / Firmware)",
|
||||
"1205": "Redes, Conectividad e Informática (TI & Ciberseguridad)",
|
||||
"1206": "Tecnologías Inmersivas y Gemelos Digitales (VR/AR/LiDAR)",
|
||||
"1207": "Gestión Administrativa, Dir. de Proyectos (PMP/Agile)"
|
||||
}
|
||||
},
|
||||
"2000": {
|
||||
"name": "Materiales e Insumos",
|
||||
"children": {
|
||||
"2100": "Componentes Electrónicos Prototipado",
|
||||
"2200": "Equipamiento Industrial de Campo",
|
||||
"2300": "Materiales Eléctricos y de Montaje",
|
||||
"2400": "Insumos de Oficina, Aseo, Cafetería y Papelería"
|
||||
}
|
||||
},
|
||||
"3000": {
|
||||
"name": "Logística y Desplazamiento Terreno",
|
||||
"children": {
|
||||
"3101": "Combustibles / Bencina",
|
||||
"3102": "Arriendo Vehículos 4x4",
|
||||
"3103": "Pasajes de Bus Interurbano",
|
||||
"3104": "Pasajes de Micro / Urbano",
|
||||
"3105": "Pasajes de Colectivos / Taxis",
|
||||
"3106": "Uber / Cabify / Didi",
|
||||
"3107": "Peajes & Tag Nacionales",
|
||||
"3108": "Reembolso Kilometraje Particular",
|
||||
"3200": "Viáticos Alimentación y Alojamiento Terreno"
|
||||
}
|
||||
},
|
||||
"4000": {
|
||||
"name": "Software y Herramientas Digitales",
|
||||
"children": {
|
||||
"4100": "Licencias de Ingeniería & Diseño (Altium/Unity/Unreal)",
|
||||
"4200": "Infraestructura y Servidores Cloud (AWS/Azure)",
|
||||
"4300": "Herramientas de Gestión de Proyectos / Software General"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Create Parents and Children
|
||||
for p_code, p_data in cbs_structure.items():
|
||||
parent_cat, _ = Category.objects.get_or_create(
|
||||
code=p_code,
|
||||
defaults={'name': p_data['name'], 'description': 'Partida Principal (Macro-categoría)'}
|
||||
)
|
||||
print(f"Created Parent: {parent_cat.code} - {parent_cat.name}")
|
||||
|
||||
for c_code, c_name in p_data['children'].items():
|
||||
child_cat, _ = Category.objects.get_or_create(
|
||||
code=c_code,
|
||||
defaults={'name': c_name, 'parent': parent_cat}
|
||||
)
|
||||
print(f" Created Child: {child_cat.code} - {child_cat.name}")
|
||||
|
||||
print("✅ CBS Seeding completed successfully.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_seed()
|
||||
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
import django
|
||||
import sys
|
||||
|
||||
# Set up Django environment
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
django.setup()
|
||||
|
||||
from expenses.models import Project
|
||||
|
||||
def run_seed():
|
||||
print("Clearing existing projects to ensure clean portfolio seeding...")
|
||||
# This might cascade to expenses, which is expected for seeding a clean MVP/dev environment
|
||||
Project.objects.all().delete()
|
||||
|
||||
print("Seeding Portfolios and Projects...")
|
||||
|
||||
# Portfolio definitions: name and number of projects
|
||||
portfolio_def = {
|
||||
"Portafolio 1": 5,
|
||||
"Portafolio 2": 4,
|
||||
"Portafolio 3": 3,
|
||||
"Portafolio 4": 1,
|
||||
"Portafolio 5": 1,
|
||||
"Portafolio 6": 1,
|
||||
"Portafolio 7": 2,
|
||||
}
|
||||
|
||||
for index, (port_name, num_projects) in enumerate(portfolio_def.items(), 1):
|
||||
port_code = f"PORT-{index:03d}"
|
||||
|
||||
# Create portfolio (parent)
|
||||
portfolio = Project.objects.create(
|
||||
code=port_code,
|
||||
name=port_name,
|
||||
description=f"Portafolio principal para agrupar {num_projects} proyectos contables.",
|
||||
assigned_budget_clp=50000000.00, # 50M base portfolio budget
|
||||
parent=None
|
||||
)
|
||||
print(f"Created Portfolio: {portfolio.code} - {portfolio.name}")
|
||||
|
||||
# Create subprojects
|
||||
for sub_idx in range(1, num_projects + 1):
|
||||
sub_code = f"PRY-{index}{sub_idx:02d}"
|
||||
subproject = Project.objects.create(
|
||||
code=sub_code,
|
||||
name=f"Proyecto {sub_idx} ({port_name})",
|
||||
description=f"Subproyecto contable imputable de {port_name}.",
|
||||
assigned_budget_clp=10000000.00, # 10M base subproject budget
|
||||
parent=portfolio
|
||||
)
|
||||
print(f" Created Project: {subproject.code} - {subproject.name}")
|
||||
|
||||
print("✅ Portfolios and Projects seeding completed successfully.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_seed()
|
||||
@@ -0,0 +1,63 @@
|
||||
import os
|
||||
import django
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
django.setup()
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from expenses.models import Project, Category, UserProfile
|
||||
|
||||
def seed():
|
||||
print("Seeding project categories and user specialties...")
|
||||
|
||||
# Load categories
|
||||
cat_1201 = Category.objects.filter(code="1201").first()
|
||||
cat_1202 = Category.objects.filter(code="1202").first()
|
||||
cat_1203 = Category.objects.filter(code="1203").first()
|
||||
cat_1204 = Category.objects.filter(code="1204").first()
|
||||
cat_1205 = Category.objects.filter(code="1205").first()
|
||||
|
||||
# Link categories to some projects
|
||||
projs = Project.objects.filter(parent__isnull=False)
|
||||
if projs.exists():
|
||||
# Set category on a few subprojects
|
||||
mapping = {
|
||||
"PRY-101": (cat_1201, "ACTIVO"),
|
||||
"PRY-102": (cat_1202, "PAUSADA"),
|
||||
"PRY-103": (cat_1203, "ACTIVO"),
|
||||
"PRY-104": (cat_1204, "TERMINADO"),
|
||||
"PRY-201": (cat_1201, "ACTIVO"),
|
||||
"PRY-202": (cat_1202, "ACTIVO"),
|
||||
"PRY-301": (cat_1205, "PAUSADA"),
|
||||
}
|
||||
for code, (cat, status) in mapping.items():
|
||||
proj = Project.objects.filter(code=code).first()
|
||||
if proj and cat:
|
||||
proj.category = cat
|
||||
proj.status = status
|
||||
proj.save()
|
||||
print(f"Set Project {proj.code} Category to {cat.code} and Status to {status}")
|
||||
|
||||
# Set specialties on users
|
||||
miguel = User.objects.filter(username="miguel_contreras").first()
|
||||
ginevra = User.objects.filter(username="ginevra_ortega").first()
|
||||
admin = User.objects.filter(username="admin").first()
|
||||
|
||||
if miguel and hasattr(miguel, 'profile'):
|
||||
miguel.profile.specialties.set([cat_1201, cat_1202])
|
||||
print(f"Set miguel_contreras specialties to 1201, 1202")
|
||||
|
||||
if ginevra and hasattr(ginevra, 'profile'):
|
||||
ginevra.profile.specialties.set([cat_1202, cat_1203, cat_1204])
|
||||
print(f"Set ginevra_ortega specialties to 1202, 1203, 1204")
|
||||
|
||||
if admin and hasattr(admin, 'profile'):
|
||||
admin.profile.specialties.set([cat_1205])
|
||||
print(f"Set admin specialties to 1205")
|
||||
|
||||
print("✅ Project categories and user specialties seeding completed.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed()
|
||||
@@ -55,6 +55,11 @@
|
||||
<a href="{% url 'project_list' %}" class="nav-tab {% if request.resolver_match.url_name == 'project_list' %}active{% endif %}">
|
||||
<i data-lucide="folder-kanban" style="width:16px;"></i> Control de Proyectos
|
||||
</a>
|
||||
{% if 'Administrador Finanzas' in request.user.groups.all.0.name or 'Administrador' in request.user.groups.all.0.name or request.user.is_superuser %}
|
||||
<a href="{% url 'accounts_payable' %}" class="nav-tab {% if request.resolver_match.url_name == 'accounts_payable' %}active{% endif %}">
|
||||
<i data-lucide="calculator" style="width:16px;"></i> Cuentas por Pagar (CXP)
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if 'Administrador' in request.user.groups.all.0.name or request.user.is_superuser %}
|
||||
<a href="{% url 'gestion_panel' %}" class="nav-tab {% if request.resolver_match.url_name == 'gestion_panel' %}active{% endif %}" style="margin-left:auto; background:var(--efintel-petroleum); color:white;">
|
||||
<i data-lucide="settings" style="width:16px;"></i> Gestión ERP
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2>Cuentas por Pagar (CXP)</h2>
|
||||
<p>Consolidado de deudas por proveedor en estado Solicitado a Finanzas.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="main-content">
|
||||
<div class="data-card">
|
||||
<div class="data-table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>RUT</th>
|
||||
<th>Razón Social</th>
|
||||
<th>Datos de Pago</th>
|
||||
<th>Cantidad de Facturas</th>
|
||||
<th>Total Deuda (CLP)</th>
|
||||
<th>Total Deuda (USD)</th>
|
||||
<th>Acción Sugerida</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for debt in debts %}
|
||||
<tr>
|
||||
<td style="font-weight: 600; color: var(--efintel-blue);">{{ debt.vendor__rut }}</td>
|
||||
<td style="font-weight: 600;">{{ debt.vendor__name }}</td>
|
||||
<td>
|
||||
{% if debt.vendor__bank %}
|
||||
<div style="font-size: 0.85rem;">{{ debt.vendor__bank }}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--efintel-gray);">{{ debt.vendor__account_type }} - {{ debt.vendor__account_number }}</div>
|
||||
{% else %}
|
||||
<em style="color: var(--efintel-gray);">No registrados</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align: center;"><span class="badge badge-amber">{{ debt.expense_count }} pendientes</span></td>
|
||||
<td class="amount-cell" style="font-size: 1rem; color: var(--efintel-danger);">
|
||||
${{ debt.total_clp|floatformat:"0"|intcomma }}
|
||||
</td>
|
||||
<td class="amount-cell" style="font-size: 0.85rem; color: var(--efintel-gray);">
|
||||
${{ debt.total_usd|floatformat:"2"|intcomma }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'expense_list' %}?vendor={{ debt.vendor__id }}" class="btn-primary" style="font-size: 0.75rem;">Ver Detalle</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" style="text-align: center; padding: 3rem; color: var(--efintel-gray);">
|
||||
No hay cuentas por pagar pendientes en este momento.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,62 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container" style="max-width: 800px; margin: 0 auto;">
|
||||
<div class="data-card" style="padding: 2.5rem; background: white; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 2rem; border-bottom: 2px solid var(--efintel-light); padding-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<i data-lucide="{% if form.instance.pk %}edit-3{% else %}plus-circle{% endif %}"></i>
|
||||
{% if form.instance.pk %}Editar Categoría Contable{% else %}Nueva Categoría Contable{% endif %}
|
||||
</h2>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.code.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Código</label>
|
||||
{{ form.code }}
|
||||
{% if form.code.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.code.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.name.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Nombre de Categoría</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.name.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||
<label for="{{ form.parent.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Partida (Categoría Padre)</label>
|
||||
<p style="font-size: 0.75rem; color: var(--text-secondary); margin-bottom: 0.25rem;">Dejar en blanco si esta es una Partida principal.</p>
|
||||
{{ form.parent }}
|
||||
{% if form.parent.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.parent.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 2rem;">
|
||||
<label for="{{ form.description.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Descripción (Opcional)</label>
|
||||
{{ form.description }}
|
||||
{% if form.description.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.description.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
|
||||
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem;">Guardar Categoría</button>
|
||||
<a href="{% url 'gestion_panel' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
||||
el.classList.add('form-control');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
@@ -20,9 +21,9 @@
|
||||
<span class="kpi-title">EJECUCIÓN HISTÓRICA / MES</span>
|
||||
<div class="kpi-icon"><i data-lucide="credit-card"></i></div>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-spend">${{ total_all|floatformat:"0" }} CLP</div>
|
||||
<div class="kpi-value" id="kpi-spend">${{ total_all|floatformat:"0"|intcomma }} CLP</div>
|
||||
<div class="kpi-subtext">
|
||||
<span>Mes actual: ${{ total_month|floatformat:"0" }} CLP</span>
|
||||
<span>Mes actual: ${{ total_month|floatformat:"0"|intcomma }} CLP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,9 +32,9 @@
|
||||
<span class="kpi-title">PRESUPUESTO GENERAL I+D+i</span>
|
||||
<div class="kpi-icon"><i data-lucide="wallet"></i></div>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-remaining">${{ global_budget|floatformat:"0" }} CLP</div>
|
||||
<div class="kpi-value" id="kpi-remaining">${{ global_budget|floatformat:"0"|intcomma }} CLP</div>
|
||||
<div class="kpi-subtext">
|
||||
<span>Saldo Restante: ${{ remaining_budget|floatformat:"0" }} CLP</span>
|
||||
<span>Saldo Restante: ${{ remaining_budget|floatformat:"0"|intcomma }} CLP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,6 +50,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gráficos de Inteligencia de Negocios (BI) -->
|
||||
<div class="charts-grid" style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<!-- Gráfico 1: Categorías -->
|
||||
<div class="data-card" style="padding: 1.5rem;">
|
||||
<h3 style="color: var(--efintel-petroleum); font-size: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--efintel-light); padding-bottom: 0.5rem;">Distribución por Categoría</h3>
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="categoryChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gráfico 2: Proyectos -->
|
||||
<div class="data-card" style="padding: 1.5rem;">
|
||||
<h3 style="color: var(--efintel-petroleum); font-size: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--efintel-light); padding-bottom: 0.5rem;">Ejecución por Proyectos / Centros</h3>
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="projectChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card">
|
||||
<div class="data-card-header">
|
||||
<div style="font-weight:700; font-size:0.95rem; color:var(--efintel-dark);">Últimas Erogaciones Registradas (Flujo ERP)</div>
|
||||
@@ -79,8 +99,8 @@
|
||||
<span style="font-size:0.65rem; color:var(--efintel-gray);">{{ expense.responsible_person.username|default:expense.user.username }}</span>
|
||||
</td>
|
||||
<td class="amount-cell">
|
||||
<div>${{ expense.amount_clp|floatformat:"0" }} CLP</div>
|
||||
<div style="font-size:0.68rem; color:var(--efintel-gray); font-family:var(--font-mono);">${{ expense.amount_usd|floatformat:"2" }} USD</div>
|
||||
<div>${{ expense.amount_clp|floatformat:"0"|intcomma }} CLP</div>
|
||||
<div style="font-size:0.68rem; color:var(--efintel-gray); font-family:var(--font-mono);">${{ expense.amount_usd|floatformat:"2"|intcomma }} USD</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if expense.approval_status == 'EN_REVISION_DOCUMENTAL' %}
|
||||
@@ -120,3 +140,112 @@
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Colores corporativos EFINTEL
|
||||
const colors = [
|
||||
'#053B50', // petroleum
|
||||
'#176B87', // blue
|
||||
'#64CCC5', // turquoise
|
||||
'#107c41', // emerald
|
||||
'#d97706', // amber
|
||||
'#475569', // slate
|
||||
'#0f172a' // dark
|
||||
];
|
||||
|
||||
// Data inyectada desde el backend (JSON)
|
||||
const catLabels = JSON.parse('{{ category_labels_json|safe }}');
|
||||
const catData = JSON.parse('{{ category_data_json|safe }}');
|
||||
|
||||
const projLabels = JSON.parse('{{ project_labels_json|safe }}');
|
||||
const projData = JSON.parse('{{ project_data_json|safe }}');
|
||||
|
||||
// 1. Gráfico de Dona: Categorías
|
||||
if (document.getElementById('categoryChart') && catData.length > 0) {
|
||||
new Chart(document.getElementById('categoryChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: catLabels,
|
||||
datasets: [{
|
||||
data: catData,
|
||||
backgroundColor: colors,
|
||||
borderWidth: 0,
|
||||
hoverOffset: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'right', labels: { boxWidth: 12, font: { family: 'Inter', size: 11 } } },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let value = context.raw || 0;
|
||||
return ' $' + value.toLocaleString('es-CL') + ' CLP';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
} else if(document.getElementById('categoryChart')) {
|
||||
document.getElementById('categoryChart').parentElement.innerHTML = '<div style="display:flex; height:100%; align-items:center; justify-content:center; color:#94a3b8;">Sin datos suficientes</div>';
|
||||
}
|
||||
|
||||
// 2. Gráfico de Barras: Proyectos
|
||||
if (document.getElementById('projectChart') && projData.length > 0) {
|
||||
new Chart(document.getElementById('projectChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: projLabels,
|
||||
datasets: [{
|
||||
label: 'Gasto Acumulado (CLP)',
|
||||
data: projData,
|
||||
backgroundColor: '#176B87',
|
||||
borderRadius: 4,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let value = context.raw || 0;
|
||||
return ' $' + value.toLocaleString('es-CL') + ' CLP';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: '#f1f5f9', drawBorder: false },
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
if (value >= 1000000) return '$' + (value / 1000000).toFixed(1) + 'M';
|
||||
if (value >= 1000) return '$' + (value / 1000).toFixed(0) + 'k';
|
||||
return '$' + value;
|
||||
},
|
||||
font: { family: 'Inter', size: 10 }
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { font: { family: 'Inter', size: 11 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if(document.getElementById('projectChart')) {
|
||||
document.getElementById('projectChart').parentElement.innerHTML = '<div style="display:flex; height:100%; align-items:center; justify-content:center; color:#94a3b8;">Sin datos suficientes</div>';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,15 +12,27 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<div class="form-group" style="position: relative;">
|
||||
<label for="{{ form.project.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Centro de Costo / Proyecto</label>
|
||||
{{ form.project }}
|
||||
<div class="custom-select-wrapper" style="position: relative;">
|
||||
{{ form.project }}
|
||||
<input type="text" id="project-search-input" placeholder="Escriba código o nombre del proyecto..." autocomplete="off">
|
||||
<div id="project-dropdown-list" class="custom-dropdown-list" style="display: none; position: absolute; background: white; border: 1px solid var(--efintel-border); border-radius: 4px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 100%; z-index: 1000; max-height: 250px; overflow-y: auto; margin-top: 4px; box-sizing: border-box;">
|
||||
<!-- Items dynamically populated -->
|
||||
</div>
|
||||
</div>
|
||||
{% if form.project.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.project.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" style="position: relative;">
|
||||
<label for="{{ form.category.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Categoría Contable</label>
|
||||
{{ form.category }}
|
||||
<div class="custom-select-wrapper" style="position: relative;">
|
||||
{{ form.category }}
|
||||
<input type="text" id="category-search-input" placeholder="Escriba código o nombre para buscar..." autocomplete="off">
|
||||
<div id="category-dropdown-list" class="custom-dropdown-list" style="display: none; position: absolute; background: white; border: 1px solid var(--efintel-border); border-radius: 4px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 100%; z-index: 1000; max-height: 250px; overflow-y: auto; margin-top: 4px; box-sizing: border-box;">
|
||||
<!-- Items dynamically populated -->
|
||||
</div>
|
||||
</div>
|
||||
{% if form.category.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.category.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,10 +41,32 @@
|
||||
<div class="form-group">
|
||||
<label for="{{ form.amount_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Monto Neto (CLP)</label>
|
||||
{{ form.amount_clp }}
|
||||
<small style="color: var(--efintel-gray); font-size: 0.7rem;">El equivalente en USD será calculado automáticamente por el sistema.</small>
|
||||
{% if form.amount_clp.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.amount_clp.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.tax_type.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Tipo de Impuesto</label>
|
||||
{{ form.tax_type }}
|
||||
{% if form.tax_type.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.tax_type.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.tax_amount_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Monto Impuesto (CLP)</label>
|
||||
{{ form.tax_amount_clp }}
|
||||
{% if form.tax_amount_clp.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.tax_amount_clp.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.total_amount_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Monto Total (CLP)</label>
|
||||
{{ form.total_amount_clp }}
|
||||
<small style="color: var(--efintel-gray); font-size: 0.7rem;">El equivalente en USD será calculado automáticamente por el sistema.</small>
|
||||
{% if form.total_amount_clp.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.total_amount_clp.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.currency.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Moneda Original</label>
|
||||
{{ form.currency }}
|
||||
@@ -110,5 +144,201 @@
|
||||
el.style.fontSize = '0.9rem';
|
||||
el.style.boxSizing = 'border-box';
|
||||
});
|
||||
|
||||
const amountInput = document.getElementById('id_amount_clp');
|
||||
const taxTypeSelect = document.getElementById('id_tax_type');
|
||||
const taxAmountInput = document.getElementById('id_tax_amount_clp');
|
||||
const totalAmountInput = document.getElementById('id_total_amount_clp');
|
||||
|
||||
function calculateTaxes() {
|
||||
const amount = parseFloat(amountInput.value) || 0;
|
||||
const taxType = taxTypeSelect.value;
|
||||
let taxAmount = 0;
|
||||
|
||||
if (taxType === 'IVA_19') {
|
||||
taxAmount = amount * 0.19;
|
||||
} else if (taxType === 'HONORARIOS_13_75') {
|
||||
taxAmount = amount * 0.1375;
|
||||
}
|
||||
|
||||
taxAmountInput.value = taxAmount.toFixed(0);
|
||||
totalAmountInput.value = (amount + taxAmount).toFixed(0);
|
||||
}
|
||||
|
||||
amountInput.addEventListener('input', calculateTaxes);
|
||||
taxTypeSelect.addEventListener('change', calculateTaxes);
|
||||
|
||||
// Initial calculation
|
||||
calculateTaxes();
|
||||
|
||||
// Custom Searchable Dropdown for Category
|
||||
const categorySelect = document.getElementById('id_category');
|
||||
const searchInput = document.getElementById('category-search-input');
|
||||
const dropdownList = document.getElementById('category-dropdown-list');
|
||||
|
||||
if (categorySelect && searchInput && dropdownList) {
|
||||
// Hide original select (make it display none so form data is still submitted)
|
||||
categorySelect.style.display = 'none';
|
||||
|
||||
// Extract options from the django-generated select
|
||||
const options = Array.from(categorySelect.options)
|
||||
.filter(opt => opt.value !== '')
|
||||
.map(opt => ({
|
||||
value: opt.value,
|
||||
text: opt.textContent
|
||||
}));
|
||||
|
||||
// Render option list dynamically
|
||||
function renderList(filteredOptions) {
|
||||
dropdownList.innerHTML = '';
|
||||
if (filteredOptions.length === 0) {
|
||||
dropdownList.innerHTML = '<div style="padding: 0.65rem; color: var(--efintel-gray); font-size: 0.85rem; text-align: center;">No se encontraron categorías</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
filteredOptions.forEach(opt => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'dropdown-item';
|
||||
item.style.padding = '0.65rem 1rem';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.fontSize = '0.85rem';
|
||||
item.style.borderBottom = '1px solid #f1f5f9';
|
||||
item.textContent = opt.text;
|
||||
|
||||
// Mark parent categories differently (they usually have a badge in representation or just end with standard code)
|
||||
// Note: we can detect if it's a main partida (ends in '000: ' or contains 'Partida')
|
||||
const isParent = opt.text.includes('1000: ') || opt.text.includes('2000: ') || opt.text.includes('3000: ') || opt.text.includes('4000: ');
|
||||
if (isParent) {
|
||||
item.style.fontWeight = '700';
|
||||
item.style.background = '#f8fafc';
|
||||
item.style.color = 'var(--efintel-petroleum)';
|
||||
}
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
categorySelect.value = opt.value;
|
||||
searchInput.value = opt.text;
|
||||
dropdownList.style.display = 'none';
|
||||
categorySelect.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
item.addEventListener('mouseenter', () => {
|
||||
item.style.background = 'var(--efintel-light)';
|
||||
item.style.color = 'var(--efintel-blue)';
|
||||
});
|
||||
item.addEventListener('mouseleave', () => {
|
||||
item.style.background = isParent ? '#f8fafc' : 'white';
|
||||
item.style.color = isParent ? 'var(--efintel-petroleum)' : 'inherit';
|
||||
});
|
||||
|
||||
dropdownList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize input value if edit mode
|
||||
const selectedOption = categorySelect.options[categorySelect.selectedIndex];
|
||||
if (selectedOption && selectedOption.value !== '') {
|
||||
searchInput.value = selectedOption.textContent;
|
||||
}
|
||||
|
||||
// Show dropdown list on focus
|
||||
searchInput.addEventListener('focus', () => {
|
||||
renderList(options);
|
||||
dropdownList.style.display = 'block';
|
||||
});
|
||||
|
||||
// Dynamic filtering
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = options.filter(opt => opt.text.toLowerCase().includes(query));
|
||||
renderList(filtered);
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!searchInput.contains(e.target) && !dropdownList.contains(e.target)) {
|
||||
dropdownList.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
// Custom Searchable Dropdown for Project
|
||||
const projectSelect = document.getElementById('id_project');
|
||||
const projectSearchInput = document.getElementById('project-search-input');
|
||||
const projectDropdownList = document.getElementById('project-dropdown-list');
|
||||
|
||||
if (projectSelect && projectSearchInput && projectDropdownList) {
|
||||
projectSelect.style.display = 'none';
|
||||
|
||||
const projectOptions = Array.from(projectSelect.options)
|
||||
.filter(opt => opt.value !== '')
|
||||
.map(opt => ({
|
||||
value: opt.value,
|
||||
text: opt.textContent
|
||||
}));
|
||||
|
||||
function renderProjectList(filteredOptions) {
|
||||
projectDropdownList.innerHTML = '';
|
||||
if (filteredOptions.length === 0) {
|
||||
projectDropdownList.innerHTML = '<div style="padding: 0.65rem; color: var(--efintel-gray); font-size: 0.85rem; text-align: center;">No se encontraron proyectos</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
filteredOptions.forEach(opt => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'dropdown-item';
|
||||
item.style.padding = '0.65rem 1rem';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.fontSize = '0.85rem';
|
||||
item.style.borderBottom = '1px solid #f1f5f9';
|
||||
item.textContent = opt.text;
|
||||
|
||||
const isPortfolio = opt.text.startsWith('PORT-');
|
||||
if (isPortfolio) {
|
||||
item.style.fontWeight = '700';
|
||||
item.style.background = '#f8fafc';
|
||||
item.style.color = 'var(--efintel-petroleum)';
|
||||
}
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
projectSelect.value = opt.value;
|
||||
projectSearchInput.value = opt.text;
|
||||
projectDropdownList.style.display = 'none';
|
||||
projectSelect.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
item.addEventListener('mouseenter', () => {
|
||||
item.style.background = 'var(--efintel-light)';
|
||||
item.style.color = 'var(--efintel-blue)';
|
||||
});
|
||||
item.addEventListener('mouseleave', () => {
|
||||
item.style.background = isPortfolio ? '#f8fafc' : 'white';
|
||||
item.style.color = isPortfolio ? 'var(--efintel-petroleum)' : 'inherit';
|
||||
});
|
||||
|
||||
projectDropdownList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
const selectedProjectOption = projectSelect.options[projectSelect.selectedIndex];
|
||||
if (selectedProjectOption && selectedProjectOption.value !== '') {
|
||||
projectSearchInput.value = selectedProjectOption.textContent;
|
||||
}
|
||||
|
||||
projectSearchInput.addEventListener('focus', () => {
|
||||
renderProjectList(projectOptions);
|
||||
projectDropdownList.style.display = 'block';
|
||||
});
|
||||
|
||||
projectSearchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = projectOptions.filter(opt => opt.text.toLowerCase().includes(query));
|
||||
renderProjectList(filtered);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!projectSearchInput.contains(e.target) && !projectDropdownList.contains(e.target)) {
|
||||
projectDropdownList.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
@@ -71,8 +72,8 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="amount-cell">
|
||||
<div>${{ expense.amount_clp|floatformat:"0" }} CLP</div>
|
||||
<div style="font-size:0.68rem; color:var(--efintel-gray); font-family:var(--font-mono);">${{ expense.amount_usd|floatformat:"2" }} USD</div>
|
||||
<div>${{ expense.amount_clp|floatformat:"0"|intcomma }} CLP</div>
|
||||
<div style="font-size:0.68rem; color:var(--efintel-gray); font-family:var(--font-mono);">${{ expense.amount_usd|floatformat:"2"|intcomma }} USD</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if expense.approval_status == 'EN_REVISION_DOCUMENTAL' %}
|
||||
@@ -96,15 +97,19 @@
|
||||
<a href="{% url 'approve_expense' expense.id 'rechazar' %}" class="btn-danger" style="padding:0.3rem; font-size:0.65rem;" title="Rechazar"><i data-lucide="x"></i></a>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.groups.all.0.name == 'Administrador' or request.user.is_superuser or expense.user == request.user %}
|
||||
<a href="/gastos/{{ expense.id }}/editar/" class="btn-secondary" style="padding: 0.3rem; background: var(--efintel-light); color: var(--efintel-petroleum);" title="Editar"><i data-lucide="pencil" style="width:14px;"></i></a>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.groups.all.0.name == 'Administrador' or request.user.is_superuser %}
|
||||
<form method="post" action="/gastos/{{ expense.id }}/eliminar/" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-danger" style="padding: 0.3rem;" onclick="return confirm('¿Seguro que desea eliminar esta erogación? Esta acción quedará en el Log de Auditoría.');" title="Eliminar"><i data-lucide="trash-2" style="width:14px;"></i></button>
|
||||
</form>
|
||||
{% if expense.approval_status != 'SOLICITADO_FINANZAS' and expense.approval_status != 'LIBERADO_PAGADO' %}
|
||||
{% if request.user.groups.all.0.name == 'Administrador' or request.user.is_superuser or expense.user == request.user %}
|
||||
<a href="/gastos/{{ expense.id }}/editar/" class="btn-secondary" style="padding: 0.3rem; background: var(--efintel-light); color: var(--efintel-petroleum);" title="Editar"><i data-lucide="pencil" style="width:14px;"></i></a>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.groups.all.0.name == 'Administrador' or request.user.is_superuser %}
|
||||
<form method="post" action="/gastos/{{ expense.id }}/eliminar/" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-danger" style="padding: 0.3rem;" onclick="return confirm('¿Seguro que desea eliminar esta erogación? Esta acción quedará en el Log de Auditoría.');" title="Eliminar"><i data-lucide="trash-2" style="width:14px;"></i></button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span style="font-size:0.75rem; color:var(--efintel-gray); padding: 0.3rem;" title="Comprobante Bloqueado"><i data-lucide="lock" style="width:14px;"></i></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
+307
-47
@@ -1,41 +1,149 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<div class="header-section" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
||||
<h1>Panel de Gestión Administrativa</h1>
|
||||
</div>
|
||||
|
||||
<div class="glass-card tabs-container" style="padding: 2rem;">
|
||||
<nav class="tabs-nav">
|
||||
<button class="tab-btn active" onclick="switchTab(event, 'tab-proyectos')">📁 Proyectos</button>
|
||||
<button class="tab-btn" onclick="switchTab(event, 'tab-responsables')">👥 Colaboradores / Responsables</button>
|
||||
<button class="tab-btn" onclick="switchTab(event, 'tab-config')">⚙️ Configuración</button>
|
||||
<button class="tab-btn" onclick="switchTab(event, 'tab-bitacora')">📜 Bitácora de Auditoría</button>
|
||||
</nav>
|
||||
<style>
|
||||
.sidebar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0.8rem 1rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--efintel-dark);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.sidebar-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
color: var(--efintel-blue);
|
||||
}
|
||||
.sidebar-btn.active {
|
||||
background: rgba(5, 59, 80, 0.08); /* efintel-petroleum with opacity */
|
||||
color: var(--efintel-petroleum);
|
||||
font-weight: 600;
|
||||
border-left: 4px solid var(--efintel-petroleum);
|
||||
}
|
||||
.sidebar-btn i {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="gestion-layout" style="display: grid; grid-template-columns: 260px 1fr; gap: 2rem; align-items: start;">
|
||||
|
||||
<!-- Sidebar Navigation -->
|
||||
<div class="data-card" style="padding: 1rem; position: sticky; top: 2rem; background: white; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
|
||||
<nav class="sidebar-nav" style="display: flex; flex-direction: column; gap: 0.25rem;">
|
||||
<button class="sidebar-btn active" onclick="switchTab(event, 'tab-proyectos')">
|
||||
<i data-lucide="folder-kanban"></i> Proyectos y Centros
|
||||
</button>
|
||||
<button class="sidebar-btn" onclick="switchTab(event, 'tab-categorias')">
|
||||
<i data-lucide="tags"></i> Categorías Contables
|
||||
</button>
|
||||
<button class="sidebar-btn" onclick="switchTab(event, 'tab-proveedores')">
|
||||
<i data-lucide="building-2"></i> Proveedores
|
||||
</button>
|
||||
<button class="sidebar-btn" onclick="switchTab(event, 'tab-responsables')">
|
||||
<i data-lucide="users"></i> Responsables
|
||||
</button>
|
||||
<hr style="border: none; border-top: 1px solid var(--efintel-light); margin: 0.75rem 0;" />
|
||||
<button class="sidebar-btn" onclick="switchTab(event, 'tab-config')">
|
||||
<i data-lucide="settings"></i> Configuración General
|
||||
</button>
|
||||
<button class="sidebar-btn" onclick="switchTab(event, 'tab-bitacora')">
|
||||
<i data-lucide="scroll-text"></i> Bitácora de Auditoría
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="gestion-content">
|
||||
|
||||
<!-- Pestaña Proyectos -->
|
||||
<div id="tab-proyectos" class="tab-content active">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<h2>Proyectos del Sistema</h2>
|
||||
<a href="{% url 'project_create' %}" class="btn btn-primary">➕ Nuevo Proyecto</a>
|
||||
<h2>Proyectos y Centros de Costo</h2>
|
||||
<a href="{% url 'project_create' %}" class="btn btn-primary">Nuevo Proyecto / Centro</a>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<div class="data-card">
|
||||
<div class="data-table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Nombre</th>
|
||||
<th>Descripción</th>
|
||||
<th>Portafolio / Jerarquía</th>
|
||||
<th>Categoría / Especialidad</th>
|
||||
<th>Estado</th>
|
||||
<th>Especialistas Habilitados</th>
|
||||
<th>Encargado (Responsable)</th>
|
||||
<th>Creado Por</th>
|
||||
<th>Última Modificación</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for project in projects %}
|
||||
<tr>
|
||||
<td style="font-weight: 600; color: var(--accent-secondary);">{{ project.name }}</td>
|
||||
<td>{{ project.description|default:"Sin descripción" }}</td>
|
||||
<td style="font-weight: 600; color: var(--efintel-blue);">{{ project.code }}</td>
|
||||
<td style="font-weight: 600; color: var(--accent-secondary);">
|
||||
{% if project.parent %}<span style="color:var(--efintel-gray); margin-right:5px;">↳</span>{% endif %}
|
||||
{{ project.name }}
|
||||
</td>
|
||||
<td>
|
||||
{% if project.parent %}
|
||||
<span style="font-size: 0.85rem; color: var(--efintel-blue); font-weight: 500;">{{ project.parent.code }} - {{ project.parent.name }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-emerald" style="font-size: 0.75rem;">Portafolio Principal</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if project.category %}
|
||||
<span style="font-size: 0.8rem; font-weight: 600; color: var(--efintel-petroleum);">{{ project.category.code }} - {{ project.category.name }}</span>
|
||||
{% else %}
|
||||
<span style="color: var(--efintel-gray); font-size: 0.75rem; font-style: italic;">Sin asignar</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if project.status == 'ACTIVO' %}
|
||||
<span class="badge badge-emerald"><i class="status-dot green"></i> Activo</span>
|
||||
{% elif project.status == 'PAUSADA' %}
|
||||
<span class="badge badge-amber"><i class="status-dot orange"></i> En Pausa</span>
|
||||
{% else %}
|
||||
<span class="badge badge-red"><i class="status-dot red"></i> Terminado</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if project.category %}
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; max-width: 250px;">
|
||||
{% for spec in project.category.specialists.all %}
|
||||
<span class="badge badge-blue" style="font-size: 0.7rem; font-weight: 600; padding: 0.15rem 0.35rem; border-radius: 4px;">{{ spec.user.username }}</span>
|
||||
{% empty %}
|
||||
<span style="color: var(--efintel-gray); font-size: 0.75rem; font-style: italic;">Sin especialistas</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span style="color: var(--efintel-gray); font-size: 0.75rem; font-style: italic;">Asigna una categoría para ver especialistas</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if project.manager %}
|
||||
<span style="font-weight: 500;">{{ project.manager.username }}</span>
|
||||
@@ -43,44 +151,181 @@
|
||||
<em style="color: var(--text-secondary);">No asignado</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ project.created_by.username|default:"Sistema" }}</td>
|
||||
<td>{{ project.updated_at|date:"d/m/Y H:i" }}</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<a href="{% url 'project_edit' project.id %}" class="btn btn-outline" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;">✏️ Editar</a>
|
||||
<div style="display: flex; gap: 0.25rem;">
|
||||
<a href="{% url 'project_edit' project.id %}" class="btn-icon" title="Editar" style="color: var(--efintel-blue); padding: 0.4rem; border-radius: 4px; transition: background 0.2s;">
|
||||
<i data-lucide="edit-3" style="width: 16px;"></i>
|
||||
</a>
|
||||
<form action="{% url 'project_delete' project.id %}" method="POST" style="display: inline;" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este proyecto? Se eliminarán todos los gastos asociados.');">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;">🗑️ Eliminar</button>
|
||||
<button type="submit" class="btn-icon" title="Eliminar" style="color: var(--efintel-danger); padding: 0.4rem; border-radius: 4px; transition: background 0.2s; background: transparent; border: none; cursor: pointer;">
|
||||
<i data-lucide="trash-2" style="width: 16px;"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center;">No hay proyectos registrados.</td>
|
||||
<td colspan="8" style="text-align: center;">No hay proyectos registrados.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pestaña Categorías -->
|
||||
<div id="tab-categorias" class="tab-content">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<div>
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 0.25rem;">Categorías Contables</h2>
|
||||
<p style="color: var(--efintel-gray); font-size: 0.9rem;">Estructura de imputación financiera.</p>
|
||||
</div>
|
||||
<a href="{% url 'category_create' %}" class="btn-primary" style="text-decoration: none;">
|
||||
<i data-lucide="plus-circle" style="width: 16px;"></i> Nueva Categoría
|
||||
</a>
|
||||
</div>
|
||||
<div class="data-card" style="padding: 0;">
|
||||
<div class="data-table-wrapper" style="margin: 0; padding: 1.5rem;">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Nombre</th>
|
||||
<th>Partida / Jerarquía</th>
|
||||
<th>Descripción</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cat in categories %}
|
||||
<tr>
|
||||
<td style="font-weight: 600; color: var(--accent-primary);">{{ cat.code }}</td>
|
||||
<td style="font-weight: 600; color: var(--text-primary);">
|
||||
{% if cat.parent %}<span style="color:var(--efintel-gray); margin-right:5px;">↳</span>{% endif %}
|
||||
{{ cat.name }}
|
||||
</td>
|
||||
<td>
|
||||
{% if cat.parent %}
|
||||
<span style="font-size: 0.85rem; color: var(--efintel-blue); font-weight: 500;">{{ cat.parent.code }} - {{ cat.parent.name }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-amber" style="font-size: 0.75rem;">Partida Principal</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ cat.description|default:"Sin descripción" }}</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.25rem;">
|
||||
<a href="{% url 'category_edit' cat.id %}" class="btn-icon" title="Editar" style="color: var(--efintel-blue); padding: 0.4rem; border-radius: 4px; transition: background 0.2s;">
|
||||
<i data-lucide="edit-3" style="width: 16px;"></i>
|
||||
</a>
|
||||
<form action="{% url 'category_delete' cat.id %}" method="POST" style="display: inline;" onsubmit="return confirm('¿Estás seguro de que deseas eliminar esta categoría? Se podrían perder asociaciones con los gastos.');">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-icon" title="Eliminar" style="color: var(--efintel-danger); padding: 0.4rem; border-radius: 4px; transition: background 0.2s; background: transparent; border: none; cursor: pointer;">
|
||||
<i data-lucide="trash-2" style="width: 16px;"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" style="text-align: center;">No hay categorías registradas.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pestaña Proveedores -->
|
||||
<div id="tab-proveedores" class="tab-content">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<div>
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 0.25rem;">Directorio de Proveedores</h2>
|
||||
<p style="color: var(--efintel-gray); font-size: 0.9rem;">Registro centralizado de proveedores habilitados.</p>
|
||||
</div>
|
||||
<a href="{% url 'vendor_create' %}" class="btn-primary" style="text-decoration: none;">
|
||||
<i data-lucide="plus-circle" style="width: 16px;"></i> Nuevo Proveedor
|
||||
</a>
|
||||
</div>
|
||||
<div class="data-card" style="padding: 0;">
|
||||
<div class="data-table-wrapper" style="margin: 0; padding: 1.5rem;">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>RUT</th>
|
||||
<th>Razón Social</th>
|
||||
<th>Giro Comercial</th>
|
||||
<th>Datos Bancarios</th>
|
||||
<th>Plazo Pago</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vendor in vendors %}
|
||||
<tr>
|
||||
<td style="font-weight: 600; color: var(--accent-primary);">{{ vendor.rut }}</td>
|
||||
<td style="font-weight: 600; color: var(--text-primary);">{{ vendor.name }}</td>
|
||||
<td>{{ vendor.business_line|default:"-" }}</td>
|
||||
<td>
|
||||
{% if vendor.bank %}
|
||||
<div style="font-size: 0.85rem;">{{ vendor.bank }}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary);">{{ vendor.account_type }} - {{ vendor.account_number }}</div>
|
||||
{% else %}
|
||||
<em style="color: var(--text-secondary);">No registrados</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ vendor.payment_terms }} días</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.25rem;">
|
||||
<a href="{% url 'vendor_edit' vendor.id %}" class="btn-icon" title="Editar" style="color: var(--efintel-blue); padding: 0.4rem; border-radius: 4px; transition: background 0.2s;">
|
||||
<i data-lucide="edit-3" style="width: 16px;"></i>
|
||||
</a>
|
||||
<form action="{% url 'vendor_delete' vendor.id %}" method="POST" style="display: inline;" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este proveedor? Sus gastos quedarán huérfanos.');">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-icon" title="Eliminar" style="color: var(--efintel-danger); padding: 0.4rem; border-radius: 4px; transition: background 0.2s; background: transparent; border: none; cursor: pointer;">
|
||||
<i data-lucide="trash-2" style="width: 16px;"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center;">No hay proveedores registrados.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pestaña Responsables -->
|
||||
<div id="tab-responsables" class="tab-content">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<h2>Responsables y Colaboradores</h2>
|
||||
<a href="{% url 'responsable_create' %}" class="btn btn-primary">➕ Nuevo Responsable</a>
|
||||
<div>
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 0.25rem;">Responsables y Colaboradores</h2>
|
||||
<p style="color: var(--efintel-gray); font-size: 0.9rem;">Gestión de acceso y roles del personal.</p>
|
||||
</div>
|
||||
<a href="{% url 'responsable_create' %}" class="btn-primary" style="text-decoration: none;">
|
||||
<i data-lucide="plus-circle" style="width: 16px;"></i> Nuevo Responsable
|
||||
</a>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<div class="data-card" style="padding: 0;">
|
||||
<div class="data-table-wrapper" style="margin: 0; padding: 1.5rem;">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Usuario</th>
|
||||
<th>Correo</th>
|
||||
<th>Rol / Permiso</th>
|
||||
<th>Especialidades Habilitadas (CBS)</th>
|
||||
<th>Descripción / Bio</th>
|
||||
<th>F. Registro</th>
|
||||
<th>F. Modificación</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -93,27 +338,39 @@
|
||||
{% if resp.groups.all %}
|
||||
{% with role=resp.groups.all.0.name %}
|
||||
{% if role == 'Administrador' %}
|
||||
<span class="badge badge-admin">Administrador</span>
|
||||
<span class="badge badge-red">Administrador</span>
|
||||
{% elif role == 'Director de Proyecto' %}
|
||||
<span class="badge badge-director">Director</span>
|
||||
<span class="badge badge-blue">Director</span>
|
||||
{% else %}
|
||||
<span class="badge badge-gestor">Gestor</span>
|
||||
<span class="badge badge-emerald">Gestor</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<span class="badge badge-gestor">Sin Rol</span>
|
||||
<span class="badge badge-amber">Sin Rol</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; max-width: 250px;">
|
||||
{% for spec in resp.profile.specialties.all %}
|
||||
<span class="badge badge-blue" style="font-size: 0.7rem; padding: 0.15rem 0.35rem;" title="{{ spec.name }}">{{ spec.code }}</span>
|
||||
{% empty %}
|
||||
<span style="color: var(--efintel-gray); font-size: 0.75rem; font-style: italic;">Ninguna</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ resp.profile.description|default:"Sin descripción personal" }}</td>
|
||||
<td>{{ resp.profile.created_at|date:"d/m/Y"|default:"-" }}</td>
|
||||
<td>{{ resp.profile.updated_at|date:"d/m/Y H:i"|default:"-" }}</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<a href="{% url 'responsable_edit' resp.id %}" class="btn btn-outline" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;">✏️ Editar</a>
|
||||
<div style="display: flex; gap: 0.25rem;">
|
||||
<a href="{% url 'responsable_edit' resp.id %}" class="btn-icon" title="Editar" style="color: var(--efintel-blue); padding: 0.4rem; border-radius: 4px; transition: background 0.2s;">
|
||||
<i data-lucide="edit-3" style="width: 16px;"></i>
|
||||
</a>
|
||||
{% if resp != request.user %}
|
||||
<form action="{% url 'responsable_delete' resp.id %}" method="POST" style="display: inline;" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este responsable?');">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;">🗑️ Eliminar</button>
|
||||
<button type="submit" class="btn-icon" title="Eliminar" style="color: var(--efintel-danger); padding: 0.4rem; border-radius: 4px; transition: background 0.2s; background: transparent; border: none; cursor: pointer;">
|
||||
<i data-lucide="trash-2" style="width: 16px;"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -126,21 +383,23 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pestaña Configuración -->
|
||||
<div id="tab-config" class="tab-content">
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<h2>Configuración General del Sistema</h2>
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 0.25rem;">Configuración General del Sistema</h2>
|
||||
<p style="color: var(--efintel-gray); font-size: 0.9rem;">Parámetros y límites globales del ERP.</p>
|
||||
</div>
|
||||
<div style="max-width: 500px; display: flex; flex-direction: column; gap: 1.5rem;">
|
||||
<div class="glass-card" style="padding: 1.5rem; background: rgba(0, 0, 0, 0.2);">
|
||||
<div class="data-card" style="padding: 2rem;">
|
||||
<div style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 0.5rem;">Presupuesto General Asignado</div>
|
||||
<div style="font-size: 2rem; font-weight: 700; color: var(--accent-secondary);">${{ settings.global_budget|default:"100000.00" }}</div>
|
||||
<div style="font-size: 2.5rem; font-weight: 700; color: var(--efintel-petroleum);">${{ settings.global_budget|floatformat:"0"|intcomma|default:"100000" }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'settings_edit' %}" class="btn btn-primary">✏️ Modificar Presupuesto General</a>
|
||||
<a href="{% url 'settings_edit' %}" class="btn-primary" style="text-decoration: none;">Modificar Presupuesto General</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,20 +407,20 @@
|
||||
<!-- Pestaña Bitácora -->
|
||||
<div id="tab-bitacora" class="tab-content">
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<h2>Bitácora de Auditoría y Trazabilidad (Logs)</h2>
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 0.25rem;">Bitácora de Auditoría y Trazabilidad (Logs)</h2>
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem;">Historial detallado de operaciones realizadas por los administradores en el sistema.</p>
|
||||
</div>
|
||||
<div class="log-list">
|
||||
<div class="data-card" style="padding: 1.5rem;">
|
||||
{% for log in logs %}
|
||||
<div class="log-item">
|
||||
<div class="log-info">
|
||||
<div class="log-action">
|
||||
{% if "Creado" in log.action or "Creación" in log.action %}
|
||||
🟢 {{ log.action }}
|
||||
{{ log.action }}
|
||||
{% elif "Eliminado" in log.action or "Eliminación" in log.action %}
|
||||
🔴 {{ log.action }}
|
||||
{{ log.action }}
|
||||
{% else %}
|
||||
🟡 {{ log.action }}
|
||||
{{ log.action }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="log-desc">{{ log.description }}</div>
|
||||
@@ -176,7 +435,8 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- End Main Content Area -->
|
||||
</div> <!-- End Gestion Layout -->
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
@@ -187,7 +447,7 @@
|
||||
contents.forEach(content => content.classList.remove('active'));
|
||||
|
||||
// Desactivar todos los botones de pestaña
|
||||
const buttons = document.querySelectorAll('.tab-btn');
|
||||
const buttons = document.querySelectorAll('.sidebar-btn');
|
||||
buttons.forEach(btn => btn.classList.remove('active'));
|
||||
|
||||
// Activar pestaña actual
|
||||
@@ -202,7 +462,7 @@
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const activeTab = localStorage.getItem('activeTab');
|
||||
if (activeTab && document.getElementById(activeTab)) {
|
||||
const btn = Array.from(document.querySelectorAll('.tab-btn')).find(b => b.getAttribute('onclick').includes(activeTab));
|
||||
const btn = Array.from(document.querySelectorAll('.sidebar-btn')).find(b => b.getAttribute('onclick').includes(activeTab));
|
||||
if (btn) {
|
||||
// Simular clic en el botón
|
||||
btn.click();
|
||||
|
||||
@@ -1,42 +1,98 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card" style="padding: 2.5rem;">
|
||||
<h1 style="margin-bottom: 2rem;">
|
||||
{% if form.instance.pk %}Editar Proyecto{% else %}Nuevo Proyecto{% endif %}
|
||||
</h1>
|
||||
<div class="form-container" style="max-width: 800px; margin: 0 auto;">
|
||||
<div class="data-card" style="padding: 2.5rem; background: white; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 2rem; border-bottom: 2px solid var(--efintel-light); padding-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<i data-lucide="{% if form.instance.pk %}edit-3{% else %}plus-circle{% endif %}"></i>
|
||||
{% if form.instance.pk %}Editar Proyecto o Centro de Costo{% else %}Nuevo Proyecto o Centro de Costo{% endif %}
|
||||
</h2>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.name.id_for_label }}">Nombre del Proyecto</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.name.errors }}</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.code.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Código</label>
|
||||
{{ form.code }}
|
||||
{% if form.code.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.code.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.name.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Nombre del Proyecto / Centro</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.name.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.type.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Tipo de Centro</label>
|
||||
{{ form.type }}
|
||||
{% if form.type.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.type.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.assigned_budget_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Presupuesto Asignado (CLP)</label>
|
||||
{{ form.assigned_budget_clp }}
|
||||
{% if form.assigned_budget_clp.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.assigned_budget_clp.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.category.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Categoría del Proyecto (Especialidad)</label>
|
||||
{{ form.category }}
|
||||
{% if form.category.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.category.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.status.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Estado del Proyecto</label>
|
||||
{{ form.status }}
|
||||
{% if form.status.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.status.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||
<label for="{{ form.parent.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Portafolio Padre (Opcional)</label>
|
||||
<p style="font-size: 0.75rem; color: var(--text-secondary); margin-bottom: 0.25rem;">Dejar en blanco si este proyecto actúa como un Portafolio principal.</p>
|
||||
{{ form.parent }}
|
||||
{% if form.parent.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.parent.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.description.id_for_label }}">Descripción</label>
|
||||
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||
<label for="{{ form.description.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Descripción</label>
|
||||
{{ form.description }}
|
||||
{% if form.description.errors %}
|
||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.description.errors }}</div>
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.description.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.manager.id_for_label }}">Encargado / Responsable del Proyecto</label>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 2rem;">
|
||||
<label for="{{ form.manager.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Encargado / Responsable del Proyecto</label>
|
||||
{{ form.manager }}
|
||||
{% if form.manager.errors %}
|
||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.manager.errors }}</div>
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.manager.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
<a href="{% url 'gestion_panel' %}" class="btn btn-outline">Cancelar</a>
|
||||
<div style="display: flex; gap: 1rem; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
|
||||
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem;">Guardar Proyecto</button>
|
||||
<a href="{% url 'gestion_panel' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h1>Proyectos</h1>
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2>Proyectos</h2>
|
||||
<p>Monitoreo de Centros de Costo</p>
|
||||
</div>
|
||||
<a href="{% url 'project_create' %}" class="btn-primary">Nuevo Proyecto / Centro</a>
|
||||
</div>
|
||||
|
||||
<div class="project-grid">
|
||||
@@ -15,15 +20,23 @@
|
||||
<strong>Gestor:</strong> {{ project.manager.username|default:"No asignado" }}
|
||||
</div>
|
||||
<div class="project-stats">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span style="font-size: 0.8rem; color: var(--text-secondary);">Gastos</span>
|
||||
<span style="font-weight: 600; font-size: 1.1rem;">{{ project.expense_count|default:"0" }}</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; text-align: right;">
|
||||
<span style="font-size: 0.8rem; color: var(--text-secondary);">Total</span>
|
||||
<span style="font-weight: 600; font-size: 1.1rem; color: var(--accent-secondary);">
|
||||
${{ project.total_expenses|default:"0.00" }}
|
||||
</span>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem; width: 100%;">
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span style="font-size: 0.8rem; color: var(--efintel-gray);">Presupuesto Asignado</span>
|
||||
<span style="font-weight: 700; font-family: var(--font-mono); color: var(--efintel-petroleum);">
|
||||
${{ project.assigned_budget_clp|floatformat:"0"|intcomma }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span style="font-size: 0.8rem; color: var(--efintel-gray);">Costo Real Acumulado</span>
|
||||
<span style="font-weight: 700; font-family: var(--font-mono); color: var(--efintel-danger);">
|
||||
${{ project.actual_cost_clp|floatformat:"0"|intcomma }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--efintel-border);">
|
||||
<span style="font-size: 0.8rem; color: var(--efintel-gray);">Nº Erogaciones (Facturas)</span>
|
||||
<span style="font-weight: 700;">{{ project.expense_count|default:"0" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,71 +1,108 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card" style="padding: 2.5rem;">
|
||||
<h1 style="margin-bottom: 2rem;">
|
||||
<div class="form-container" style="max-width: 800px; margin: 0 auto;">
|
||||
<div class="data-card" style="padding: 2.5rem; background: white; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 2rem; border-bottom: 2px solid var(--efintel-light); padding-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<i data-lucide="{% if edit_mode %}edit-3{% else %}plus-circle{% endif %}"></i>
|
||||
{% if edit_mode %}Editar Colaborador (Responsable): {{ target_user.username }}{% else %}Nuevo Colaborador (Responsable){% endif %}
|
||||
</h1>
|
||||
</h2>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.username.id_for_label }}">Nombre de Usuario</label>
|
||||
{{ form.username }}
|
||||
{% if form.username.errors %}
|
||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.username.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.email.id_for_label }}">Correo Electrónico</label>
|
||||
{{ form.email }}
|
||||
{% if form.email.errors %}
|
||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.email.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.password.id_for_label }}">
|
||||
Contraseña
|
||||
{% if edit_mode %}
|
||||
<span style="font-size: 0.8rem; color: var(--text-secondary);">(Opcional: Dejar en blanco para no cambiarla)</span>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.username.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Nombre de Usuario</label>
|
||||
{{ form.username }}
|
||||
{% if form.username.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.username.errors }}</div>
|
||||
{% endif %}
|
||||
</label>
|
||||
{{ form.password }}
|
||||
{% if form.password.errors %}
|
||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.password.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.email.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Correo Electrónico</label>
|
||||
{{ form.email }}
|
||||
{% if form.email.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.email.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.role.id_for_label }}">Rol / Grupo del Sistema</label>
|
||||
{{ form.role }}
|
||||
{% if form.role.errors %}
|
||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.role.errors }}</div>
|
||||
{% endif %}
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.password.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">
|
||||
Contraseña
|
||||
{% if edit_mode %}
|
||||
<span style="font-size: 0.75rem; color: var(--efintel-gray); font-weight: 400;">(Opcional)</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{{ form.password }}
|
||||
{% if form.password.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.password.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.role.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Rol / Grupo del Sistema</label>
|
||||
{{ form.role }}
|
||||
{% if form.role.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.role.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.description.id_for_label }}">Descripción / Bio del Responsable</label>
|
||||
<div class="form-group" style="margin-bottom: 2rem;">
|
||||
<label for="{{ form.description.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Descripción / Bio del Responsable</label>
|
||||
{{ form.description }}
|
||||
{% if form.description.errors %}
|
||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.description.errors }}</div>
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.description.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
<a href="{% url 'gestion_panel' %}" class="btn btn-outline">Cancelar</a>
|
||||
<div class="form-group" style="margin-bottom: 2rem;">
|
||||
<label style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Especialidades Técnicas (Categorías Asociadas)</label>
|
||||
<p style="font-size: 0.75rem; color: var(--efintel-gray); margin-bottom: 0.75rem;">Selecciona las áreas de especialidad de este colaborador en la cartera técnica.</p>
|
||||
<div class="specialties-container" style="max-height: 220px; overflow-y: auto; padding: 1rem; border: 1px solid var(--efintel-border); border-radius: 4px; background: #f8fafc;">
|
||||
<style>
|
||||
.specialties-container ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.specialties-container li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--efintel-dark);
|
||||
}
|
||||
.specialties-container input[type="checkbox"] {
|
||||
margin-top: 0.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
{{ form.specialties }}
|
||||
</div>
|
||||
{% if form.specialties.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.specialties.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
|
||||
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem;">Guardar Responsable</button>
|
||||
<a href="{% url 'gestion_panel' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Apply form-control class to all inputs
|
||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
||||
// Apply form-control class to all inputs except checkboxes
|
||||
document.querySelectorAll('input:not([type="checkbox"]), select, textarea').forEach(el => {
|
||||
el.classList.add('form-control');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container" style="max-width: 800px; margin: 0 auto;">
|
||||
<div class="data-card" style="padding: 2.5rem; background: white; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 2rem; border-bottom: 2px solid var(--efintel-light); padding-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<i data-lucide="{% if form.instance.pk %}edit-3{% else %}plus-circle{% endif %}"></i>
|
||||
{% if form.instance.pk %}Editar Proveedor{% else %}Nuevo Proveedor{% endif %}
|
||||
</h2>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<h3 style="margin-bottom: 1rem; color: var(--efintel-dark); font-size: 1rem;">Información General</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.rut.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">RUT Proveedor</label>
|
||||
{{ form.rut }}
|
||||
{% if form.rut.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.rut.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.name.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Razón Social / Nombre</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.name.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 2rem;">
|
||||
<label for="{{ form.business_line.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Giro Comercial</label>
|
||||
{{ form.business_line }}
|
||||
{% if form.business_line.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.business_line.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 2.5rem; margin-bottom: 1rem; color: var(--efintel-dark); font-size: 1rem; border-top: 1px solid var(--efintel-light); padding-top: 1.5rem;">Datos Bancarios y Facturación</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.bank.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Banco</label>
|
||||
{{ form.bank }}
|
||||
{% if form.bank.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.bank.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.account_type.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Tipo de Cuenta</label>
|
||||
{{ form.account_type }}
|
||||
{% if form.account_type.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.account_type.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.account_number.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Número de Cuenta</label>
|
||||
{{ form.account_number }}
|
||||
{% if form.account_number.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.account_number.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.email.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Correo de Pago/Facturación</label>
|
||||
{{ form.email }}
|
||||
{% if form.email.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.email.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 2rem; width: 50%;">
|
||||
<label for="{{ form.payment_terms.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Plazo de Pago (Días)</label>
|
||||
{{ form.payment_terms }}
|
||||
{% if form.payment_terms.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.payment_terms.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
|
||||
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem;">Guardar Proveedor</button>
|
||||
<a href="{% url 'gestion_panel' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
||||
el.classList.add('form-control');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user