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.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'django.contrib.humanize',
|
||||||
'channels',
|
'channels',
|
||||||
'expenses',
|
'expenses',
|
||||||
]
|
]
|
||||||
@@ -122,6 +123,8 @@ USE_I18N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
USE_THOUSAND_SEPARATOR = True
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/6.1/howto/static-files/
|
# https://docs.djangoproject.com/en/6.1/howto/static-files/
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ class ProjectAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Category)
|
@admin.register(Category)
|
||||||
class CategoryAdmin(admin.ModelAdmin):
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
list_display = ('code', 'name', 'parent')
|
list_display = ('code', 'name')
|
||||||
search_fields = ('code', 'name')
|
search_fields = ('code', 'name')
|
||||||
|
|
||||||
@admin.register(Expense)
|
@admin.register(Expense)
|
||||||
|
|||||||
+42
-3
@@ -1,6 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import User, Group
|
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):
|
class ExpenseForm(forms.ModelForm):
|
||||||
@@ -21,10 +21,22 @@ class ExpenseForm(forms.ModelForm):
|
|||||||
widget=forms.FileInput(attrs={'accept': 'image/*,.pdf'})
|
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:
|
class Meta:
|
||||||
model = Expense
|
model = Expense
|
||||||
fields = [
|
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'
|
'description', 'vendor', 'region', 'comuna', 'vehicle_ownership'
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
@@ -35,12 +47,27 @@ class ExpenseForm(forms.ModelForm):
|
|||||||
class ProjectForm(forms.ModelForm):
|
class ProjectForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
fields = ['code', 'name', 'type', 'description', 'manager', 'assigned_budget_clp']
|
fields = ['code', 'name', 'parent', 'category', 'status', 'type', 'description', 'manager', 'assigned_budget_clp']
|
||||||
widgets = {
|
widgets = {
|
||||||
'description': forms.Textarea(attrs={'rows': 3}),
|
'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 SystemSettingForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SystemSetting
|
model = SystemSetting
|
||||||
@@ -57,6 +84,12 @@ class ResponsableForm(forms.Form):
|
|||||||
rut = forms.CharField(max_length=20, label="RUT", required=False)
|
rut = forms.CharField(max_length=20, label="RUT", required=False)
|
||||||
job_title = forms.CharField(max_length=150, label="Cargo", 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)
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
self.instance = kwargs.pop('instance', None)
|
self.instance = kwargs.pop('instance', None)
|
||||||
@@ -73,6 +106,7 @@ class ResponsableForm(forms.Form):
|
|||||||
self.fields['description'].initial = self.instance.profile.description
|
self.fields['description'].initial = self.instance.profile.description
|
||||||
self.fields['rut'].initial = self.instance.profile.rut
|
self.fields['rut'].initial = self.instance.profile.rut
|
||||||
self.fields['job_title'].initial = self.instance.profile.job_title
|
self.fields['job_title'].initial = self.instance.profile.job_title
|
||||||
|
self.fields['specialties'].initial = self.instance.profile.specialties.all()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# Password is optional for edit
|
# Password is optional for edit
|
||||||
@@ -124,6 +158,11 @@ class ResponsableForm(forms.Form):
|
|||||||
profile.job_title = job_title
|
profile.job_title = job_title
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
|
# Save specialties
|
||||||
|
specialties = self.cleaned_data.get('specialties')
|
||||||
|
if specialties is not None:
|
||||||
|
profile.specialties.set(specialties)
|
||||||
|
|
||||||
return user
|
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'
|
CLP = 'CLP', 'Pesos Chilenos'
|
||||||
USD = 'USD', 'Dólares Estadounidenses'
|
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):
|
class ExpenseTypeEnum(models.TextChoices):
|
||||||
COSTO_DIRECTO = 'COSTO_DIRECTO_PROYECTO', 'Costo Directo Proyecto'
|
COSTO_DIRECTO = 'COSTO_DIRECTO_PROYECTO', 'Costo Directo Proyecto'
|
||||||
GASTO_OPERACIONAL = 'GASTO_OPERACIONAL_OVERHEAD', 'Gasto Operacional Overhead'
|
GASTO_OPERACIONAL = 'GASTO_OPERACIONAL_OVERHEAD', 'Gasto Operacional Overhead'
|
||||||
@@ -17,6 +22,11 @@ class ApprovalStatusEnum(models.TextChoices):
|
|||||||
LIBERADO_PAGADO = 'LIBERADO_PAGADO', 'Pago Liberado (Paso 3)'
|
LIBERADO_PAGADO = 'LIBERADO_PAGADO', 'Pago Liberado (Paso 3)'
|
||||||
OBSERVADO_RECHAZADO = 'OBSERVADO_RECHAZADO', 'Observado / Rechazado'
|
OBSERVADO_RECHAZADO = 'OBSERVADO_RECHAZADO', 'Observado / Rechazado'
|
||||||
|
|
||||||
|
class ProjectStatusEnum(models.TextChoices):
|
||||||
|
ACTIVO = 'ACTIVO', 'Activo'
|
||||||
|
PAUSADA = 'PAUSADA', 'En Pausa'
|
||||||
|
TERMINADO = 'TERMINADO', 'Terminado'
|
||||||
|
|
||||||
class Project(models.Model):
|
class Project(models.Model):
|
||||||
# Added core project/costcenter fields mapped from Prisma
|
# Added core project/costcenter fields mapped from Prisma
|
||||||
code = models.CharField(max_length=50, unique=True, verbose_name="Código", default='PROY-000')
|
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")
|
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")
|
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_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)")
|
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)")
|
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")
|
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")
|
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")
|
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"
|
verbose_name_plural = "Proyectos y Centros"
|
||||||
|
|
||||||
class Category(models.Model):
|
class Category(models.Model):
|
||||||
# Category / Subcategory hierarchy
|
|
||||||
code = models.CharField(max_length=10, unique=True, verbose_name="Código de Categoría")
|
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")
|
name = models.CharField(max_length=100, verbose_name="Nombre de Categoría")
|
||||||
description = models.TextField(blank=True, verbose_name="Descripción")
|
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):
|
def __str__(self):
|
||||||
return f"{self.code}: {self.name}"
|
return f"{self.code}: {self.name}"
|
||||||
@@ -57,6 +68,30 @@ class Category(models.Model):
|
|||||||
verbose_name = "Categoría"
|
verbose_name = "Categoría"
|
||||||
verbose_name_plural = "Categorías"
|
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):
|
class Expense(models.Model):
|
||||||
expense_code = models.CharField(max_length=50, unique=True, default=uuid.uuid4, verbose_name="Código Transacción")
|
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")
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='expenses', verbose_name="Proyecto")
|
||||||
@@ -64,14 +99,18 @@ class Expense(models.Model):
|
|||||||
|
|
||||||
# Financial fields
|
# Financial fields
|
||||||
amount_clp = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="Monto Neto (CLP)")
|
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")
|
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")
|
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")
|
description = models.TextField(verbose_name="Detalle de Imputación")
|
||||||
date = models.DateField(verbose_name="Fecha del Gasto")
|
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)
|
# Geographic & Logistic fields (Operaciones en terreno)
|
||||||
region = models.CharField(max_length=100, blank=True, null=True, verbose_name="Región (Chile)")
|
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")
|
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")
|
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")
|
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")
|
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")
|
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>/editar/', views.expense_edit, name='expense_edit'),
|
||||||
path('gastos/<int:pk>/eliminar/', views.expense_delete, name='expense_delete'),
|
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'),
|
path('gastos/<int:pk>/aprobar/<str:action>/', views.approve_expense, name='approve_expense'),
|
||||||
|
|
||||||
# Export / Import
|
# Export / Import
|
||||||
@@ -26,6 +27,14 @@ urlpatterns = [
|
|||||||
path('gestion/proyectos/<int:pk>/editar/', views.project_edit, name='project_edit'),
|
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/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/nuevo/', views.responsable_create, name='responsable_create'),
|
||||||
path('gestion/responsables/<int:pk>/editar/', views.responsable_edit, name='responsable_edit'),
|
path('gestion/responsables/<int:pk>/editar/', views.responsable_edit, name='responsable_edit'),
|
||||||
path('gestion/responsables/<int:pk>/eliminar/', views.responsable_delete, name='responsable_delete'),
|
path('gestion/responsables/<int:pk>/eliminar/', views.responsable_delete, name='responsable_delete'),
|
||||||
|
|||||||
+177
-4
@@ -8,9 +8,10 @@ from django.db.models import Sum, Count, F
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from django.http import JsonResponse, HttpResponse
|
from django.http import JsonResponse, HttpResponse
|
||||||
|
import json
|
||||||
|
|
||||||
from .models import Expense, Project, Category, SystemSetting, UserProfile, AuditLog, ApprovalStatusEnum, CurrencyEnum
|
from .models import Expense, Project, Category, SystemSetting, UserProfile, AuditLog, ApprovalStatusEnum, CurrencyEnum, Vendor
|
||||||
from .forms import ExpenseForm, ProjectForm, SystemSettingForm, ResponsableForm
|
from .forms import ExpenseForm, ProjectForm, SystemSettingForm, ResponsableForm, CategoryForm, VendorForm
|
||||||
from .decorators import role_required
|
from .decorators import role_required
|
||||||
from django.contrib.auth.models import User, Group
|
from django.contrib.auth.models import User, Group
|
||||||
|
|
||||||
@@ -58,6 +59,16 @@ def dashboard(request):
|
|||||||
global_budget = setting.global_budget
|
global_budget = setting.global_budget
|
||||||
remaining_budget = global_budget - total_all_clp
|
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 = {
|
context = {
|
||||||
'total_month': total_month_clp,
|
'total_month': total_month_clp,
|
||||||
'total_all': total_all_clp,
|
'total_all': total_all_clp,
|
||||||
@@ -66,6 +77,10 @@ def dashboard(request):
|
|||||||
'recent_expenses': recent_expenses,
|
'recent_expenses': recent_expenses,
|
||||||
'global_budget': global_budget,
|
'global_budget': global_budget,
|
||||||
'remaining_budget': remaining_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)
|
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.')
|
messages.error(request, 'No tienes permisos para editar este gasto.')
|
||||||
return redirect('expense_list')
|
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
|
old_amount = expense.amount_clp
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@@ -146,6 +165,11 @@ def expense_edit(request, pk):
|
|||||||
exp = form.save(commit=False)
|
exp = form.save(commit=False)
|
||||||
setting, _ = SystemSetting.objects.get_or_create(id=1)
|
setting, _ = SystemSetting.objects.get_or_create(id=1)
|
||||||
exp.amount_usd = exp.amount_clp / setting.usd_exchange_rate
|
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()
|
exp.save()
|
||||||
|
|
||||||
receipt = request.FILES.get('receipt')
|
receipt = request.FILES.get('receipt')
|
||||||
@@ -179,6 +203,10 @@ def expense_delete(request, pk):
|
|||||||
messages.error(request, 'Solo administradores pueden eliminar gastos.')
|
messages.error(request, 'Solo administradores pueden eliminar gastos.')
|
||||||
return redirect('expense_list')
|
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':
|
if request.method == 'POST':
|
||||||
code = expense.expense_code
|
code = expense.expense_code
|
||||||
expense.delete()
|
expense.delete()
|
||||||
@@ -203,6 +231,10 @@ def approve_expense(request, pk, action):
|
|||||||
|
|
||||||
if action == 'solicitar':
|
if action == 'solicitar':
|
||||||
# Step 1 -> 2
|
# 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.approval_status = ApprovalStatusEnum.SOLICITADO_FINANZAS
|
||||||
expense.elevated_by = request.user
|
expense.elevated_by = request.user
|
||||||
expense.save()
|
expense.save()
|
||||||
@@ -228,7 +260,10 @@ def approve_expense(request, pk, action):
|
|||||||
|
|
||||||
# Update Project Cost
|
# Update Project Cost
|
||||||
proj = expense.project
|
proj = expense.project
|
||||||
|
if expense.tax_type == TaxTypeEnum.IVA_19:
|
||||||
proj.actual_cost_clp = proj.actual_cost_clp + expense.amount_clp
|
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()
|
proj.save()
|
||||||
|
|
||||||
AuditLog.objects.create(
|
AuditLog.objects.create(
|
||||||
@@ -311,13 +346,17 @@ def project_list(request):
|
|||||||
@login_required
|
@login_required
|
||||||
@role_required('Administrador')
|
@role_required('Administrador')
|
||||||
def gestion_panel(request):
|
def gestion_panel(request):
|
||||||
projects = Project.objects.all().select_related('manager', 'created_by')
|
projects = Project.objects.all().select_related('manager', 'created_by', 'parent', 'category')
|
||||||
responsables = User.objects.all().select_related('profile').prefetch_related('groups')
|
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)
|
settings_obj, _ = SystemSetting.objects.get_or_create(id=1)
|
||||||
logs = AuditLog.objects.all().select_related('user')[:100]
|
logs = AuditLog.objects.all().select_related('user')[:100]
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'projects': projects,
|
'projects': projects,
|
||||||
|
'categories': categories,
|
||||||
|
'vendors': vendors,
|
||||||
'responsables': responsables,
|
'responsables': responsables,
|
||||||
'settings': settings_obj,
|
'settings': settings_obj,
|
||||||
'logs': logs,
|
'logs': logs,
|
||||||
@@ -381,6 +420,84 @@ def project_delete(request, pk):
|
|||||||
return redirect('gestion_panel')
|
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
|
@login_required
|
||||||
@role_required('Administrador')
|
@role_required('Administrador')
|
||||||
def responsable_create(request):
|
def responsable_create(request):
|
||||||
@@ -419,3 +536,59 @@ def responsable_delete(request, pk):
|
|||||||
messages.success(request, 'Responsable eliminado.')
|
messages.success(request, 'Responsable eliminado.')
|
||||||
return redirect('gestion_panel')
|
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 %}">
|
<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
|
<i data-lucide="folder-kanban" style="width:16px;"></i> Control de Proyectos
|
||||||
</a>
|
</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 %}
|
{% 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;">
|
<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
|
<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' %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="page-view active">
|
<section class="page-view active">
|
||||||
@@ -20,9 +21,9 @@
|
|||||||
<span class="kpi-title">EJECUCIÓN HISTÓRICA / MES</span>
|
<span class="kpi-title">EJECUCIÓN HISTÓRICA / MES</span>
|
||||||
<div class="kpi-icon"><i data-lucide="credit-card"></i></div>
|
<div class="kpi-icon"><i data-lucide="credit-card"></i></div>
|
||||||
</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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,9 +32,9 @@
|
|||||||
<span class="kpi-title">PRESUPUESTO GENERAL I+D+i</span>
|
<span class="kpi-title">PRESUPUESTO GENERAL I+D+i</span>
|
||||||
<div class="kpi-icon"><i data-lucide="wallet"></i></div>
|
<div class="kpi-icon"><i data-lucide="wallet"></i></div>
|
||||||
</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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,6 +50,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
<div class="data-card-header">
|
<div class="data-card-header">
|
||||||
<div style="font-weight:700; font-size:0.95rem; color:var(--efintel-dark);">Últimas Erogaciones Registradas (Flujo ERP)</div>
|
<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>
|
<span style="font-size:0.65rem; color:var(--efintel-gray);">{{ expense.responsible_person.username|default:expense.user.username }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="amount-cell">
|
<td class="amount-cell">
|
||||||
<div>${{ expense.amount_clp|floatformat:"0" }} CLP</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" }} USD</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>
|
||||||
<td>
|
<td>
|
||||||
{% if expense.approval_status == 'EN_REVISION_DOCUMENTAL' %}
|
{% if expense.approval_status == 'EN_REVISION_DOCUMENTAL' %}
|
||||||
@@ -120,3 +140,112 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% 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 %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
<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>
|
<label for="{{ form.project.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Centro de Costo / Proyecto</label>
|
||||||
|
<div class="custom-select-wrapper" style="position: relative;">
|
||||||
{{ form.project }}
|
{{ 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 %}
|
{% if form.project.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.project.errors }}</div>{% endif %}
|
||||||
</div>
|
</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>
|
<label for="{{ form.category.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Categoría Contable</label>
|
||||||
|
<div class="custom-select-wrapper" style="position: relative;">
|
||||||
{{ form.category }}
|
{{ 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 %}
|
{% if form.category.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.category.errors }}</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,10 +41,32 @@
|
|||||||
<div class="form-group">
|
<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>
|
<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 }}
|
{{ 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 %}
|
{% 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>
|
||||||
|
|
||||||
|
<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">
|
<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>
|
<label for="{{ form.currency.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Moneda Original</label>
|
||||||
{{ form.currency }}
|
{{ form.currency }}
|
||||||
@@ -110,5 +144,201 @@
|
|||||||
el.style.fontSize = '0.9rem';
|
el.style.fontSize = '0.9rem';
|
||||||
el.style.boxSizing = 'border-box';
|
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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="page-view active">
|
<section class="page-view active">
|
||||||
@@ -71,8 +72,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="amount-cell">
|
<td class="amount-cell">
|
||||||
<div>${{ expense.amount_clp|floatformat:"0" }} CLP</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" }} USD</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>
|
||||||
<td>
|
<td>
|
||||||
{% if expense.approval_status == 'EN_REVISION_DOCUMENTAL' %}
|
{% if expense.approval_status == 'EN_REVISION_DOCUMENTAL' %}
|
||||||
@@ -96,6 +97,7 @@
|
|||||||
<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>
|
<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 %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% 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 %}
|
{% 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>
|
<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 %}
|
{% endif %}
|
||||||
@@ -106,6 +108,9 @@
|
|||||||
<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>
|
<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>
|
</form>
|
||||||
{% endif %}
|
{% 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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
+306
-46
@@ -1,41 +1,149 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="header-section" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
<div class="header-section" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
||||||
<h1>Panel de Gestión Administrativa</h1>
|
<h1>Panel de Gestión Administrativa</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="glass-card tabs-container" style="padding: 2rem;">
|
<style>
|
||||||
<nav class="tabs-nav">
|
.sidebar-btn {
|
||||||
<button class="tab-btn active" onclick="switchTab(event, 'tab-proyectos')">📁 Proyectos</button>
|
display: flex;
|
||||||
<button class="tab-btn" onclick="switchTab(event, 'tab-responsables')">👥 Colaboradores / Responsables</button>
|
align-items: center;
|
||||||
<button class="tab-btn" onclick="switchTab(event, 'tab-config')">⚙️ Configuración</button>
|
gap: 0.75rem;
|
||||||
<button class="tab-btn" onclick="switchTab(event, 'tab-bitacora')">📜 Bitácora de Auditoría</button>
|
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>
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content Area -->
|
||||||
|
<div class="gestion-content">
|
||||||
|
|
||||||
<!-- Pestaña Proyectos -->
|
<!-- Pestaña Proyectos -->
|
||||||
<div id="tab-proyectos" class="tab-content active">
|
<div id="tab-proyectos" class="tab-content active">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||||
<h2>Proyectos del Sistema</h2>
|
<h2>Proyectos y Centros de Costo</h2>
|
||||||
<a href="{% url 'project_create' %}" class="btn btn-primary">➕ Nuevo Proyecto</a>
|
<a href="{% url 'project_create' %}" class="btn btn-primary">Nuevo Proyecto / Centro</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-container">
|
<div class="data-card">
|
||||||
<table>
|
<div class="data-table-wrapper">
|
||||||
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
<th>Nombre</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>Encargado (Responsable)</th>
|
||||||
<th>Creado Por</th>
|
|
||||||
<th>Última Modificación</th>
|
|
||||||
<th>Acciones</th>
|
<th>Acciones</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for project in projects %}
|
{% for project in projects %}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight: 600; color: var(--accent-secondary);">{{ project.name }}</td>
|
<td style="font-weight: 600; color: var(--efintel-blue);">{{ project.code }}</td>
|
||||||
<td>{{ project.description|default:"Sin descripción" }}</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>
|
<td>
|
||||||
{% if project.manager %}
|
{% if project.manager %}
|
||||||
<span style="font-weight: 500;">{{ project.manager.username }}</span>
|
<span style="font-weight: 500;">{{ project.manager.username }}</span>
|
||||||
@@ -43,44 +151,181 @@
|
|||||||
<em style="color: var(--text-secondary);">No asignado</em>
|
<em style="color: var(--text-secondary);">No asignado</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ project.created_by.username|default:"Sistema" }}</td>
|
|
||||||
<td>{{ project.updated_at|date:"d/m/Y H:i" }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<div style="display: flex; gap: 0.5rem;">
|
<div style="display: flex; gap: 0.25rem;">
|
||||||
<a href="{% url 'project_edit' project.id %}" class="btn btn-outline" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;">✏️ Editar</a>
|
<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.');">
|
<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 %}
|
{% 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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Pestaña Responsables -->
|
||||||
<div id="tab-responsables" class="tab-content">
|
<div id="tab-responsables" class="tab-content">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||||
<h2>Responsables y Colaboradores</h2>
|
<div>
|
||||||
<a href="{% url 'responsable_create' %}" class="btn btn-primary">➕ Nuevo Responsable</a>
|
<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>
|
</div>
|
||||||
<div class="table-container">
|
<a href="{% url 'responsable_create' %}" class="btn-primary" style="text-decoration: none;">
|
||||||
<table>
|
<i data-lucide="plus-circle" style="width: 16px;"></i> Nuevo Responsable
|
||||||
|
</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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Usuario</th>
|
<th>Usuario</th>
|
||||||
<th>Correo</th>
|
<th>Correo</th>
|
||||||
<th>Rol / Permiso</th>
|
<th>Rol / Permiso</th>
|
||||||
|
<th>Especialidades Habilitadas (CBS)</th>
|
||||||
<th>Descripción / Bio</th>
|
<th>Descripción / Bio</th>
|
||||||
<th>F. Registro</th>
|
<th>F. Registro</th>
|
||||||
<th>F. Modificación</th>
|
|
||||||
<th>Acciones</th>
|
<th>Acciones</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -93,27 +338,39 @@
|
|||||||
{% if resp.groups.all %}
|
{% if resp.groups.all %}
|
||||||
{% with role=resp.groups.all.0.name %}
|
{% with role=resp.groups.all.0.name %}
|
||||||
{% if role == 'Administrador' %}
|
{% if role == 'Administrador' %}
|
||||||
<span class="badge badge-admin">Administrador</span>
|
<span class="badge badge-red">Administrador</span>
|
||||||
{% elif role == 'Director de Proyecto' %}
|
{% elif role == 'Director de Proyecto' %}
|
||||||
<span class="badge badge-director">Director</span>
|
<span class="badge badge-blue">Director</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-gestor">Gestor</span>
|
<span class="badge badge-emerald">Gestor</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-gestor">Sin Rol</span>
|
<span class="badge badge-amber">Sin Rol</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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.description|default:"Sin descripción personal" }}</td>
|
||||||
<td>{{ resp.profile.created_at|date:"d/m/Y"|default:"-" }}</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>
|
<td>
|
||||||
<div style="display: flex; gap: 0.5rem;">
|
<div style="display: flex; gap: 0.25rem;">
|
||||||
<a href="{% url 'responsable_edit' resp.id %}" class="btn btn-outline" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;">✏️ Editar</a>
|
<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 %}
|
{% 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?');">
|
<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 %}
|
{% 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>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -128,19 +385,21 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Pestaña Configuración -->
|
<!-- Pestaña Configuración -->
|
||||||
<div id="tab-config" class="tab-content">
|
<div id="tab-config" class="tab-content">
|
||||||
<div style="margin-bottom: 1.5rem;">
|
<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>
|
||||||
<div style="max-width: 500px; display: flex; flex-direction: column; gap: 1.5rem;">
|
<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="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>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,20 +407,20 @@
|
|||||||
<!-- Pestaña Bitácora -->
|
<!-- Pestaña Bitácora -->
|
||||||
<div id="tab-bitacora" class="tab-content">
|
<div id="tab-bitacora" class="tab-content">
|
||||||
<div style="margin-bottom: 1.5rem;">
|
<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>
|
<p style="color: var(--text-secondary); font-size: 0.9rem;">Historial detallado de operaciones realizadas por los administradores en el sistema.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-list">
|
<div class="data-card" style="padding: 1.5rem;">
|
||||||
{% for log in logs %}
|
{% for log in logs %}
|
||||||
<div class="log-item">
|
<div class="log-item">
|
||||||
<div class="log-info">
|
<div class="log-info">
|
||||||
<div class="log-action">
|
<div class="log-action">
|
||||||
{% if "Creado" in log.action or "Creación" in 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 %}
|
{% elif "Eliminado" in log.action or "Eliminación" in log.action %}
|
||||||
🔴 {{ log.action }}
|
{{ log.action }}
|
||||||
{% else %}
|
{% else %}
|
||||||
🟡 {{ log.action }}
|
{{ log.action }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="log-desc">{{ log.description }}</div>
|
<div class="log-desc">{{ log.description }}</div>
|
||||||
@@ -176,7 +435,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> <!-- End Main Content Area -->
|
||||||
|
</div> <!-- End Gestion Layout -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
@@ -187,7 +447,7 @@
|
|||||||
contents.forEach(content => content.classList.remove('active'));
|
contents.forEach(content => content.classList.remove('active'));
|
||||||
|
|
||||||
// Desactivar todos los botones de pestaña
|
// 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'));
|
buttons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
|
||||||
// Activar pestaña actual
|
// Activar pestaña actual
|
||||||
@@ -202,7 +462,7 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
const activeTab = localStorage.getItem('activeTab');
|
const activeTab = localStorage.getItem('activeTab');
|
||||||
if (activeTab && document.getElementById(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) {
|
if (btn) {
|
||||||
// Simular clic en el botón
|
// Simular clic en el botón
|
||||||
btn.click();
|
btn.click();
|
||||||
|
|||||||
@@ -1,42 +1,98 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="form-container">
|
<div class="form-container" style="max-width: 800px; margin: 0 auto;">
|
||||||
<div class="glass-card" style="padding: 2.5rem;">
|
<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);">
|
||||||
<h1 style="margin-bottom: 2rem;">
|
<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;">
|
||||||
{% if form.instance.pk %}Editar Proyecto{% else %}Nuevo Proyecto{% endif %}
|
<i data-lucide="{% if form.instance.pk %}edit-3{% else %}plus-circle{% endif %}"></i>
|
||||||
</h1>
|
{% if form.instance.pk %}Editar Proyecto o Centro de Costo{% else %}Nuevo Proyecto o Centro de Costo{% endif %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.name.id_for_label }}">Nombre del Proyecto</label>
|
<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 }}
|
{{ form.name }}
|
||||||
{% if form.name.errors %}
|
{% if form.name.errors %}
|
||||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.name.errors }}</div>
|
<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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.description.id_for_label }}">Descripción</label>
|
<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" 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 }}
|
{{ form.description }}
|
||||||
{% if form.description.errors %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group" style="margin-bottom: 2rem;">
|
||||||
<label for="{{ form.manager.id_for_label }}">Encargado / Responsable del Proyecto</label>
|
<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 }}
|
{{ form.manager }}
|
||||||
{% if form.manager.errors %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
|
<div style="display: flex; gap: 1rem; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
|
||||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
<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 btn-outline">Cancelar</a>
|
<a href="{% url 'gestion_panel' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div style="margin-bottom: 2rem;">
|
<div class="view-header">
|
||||||
<h1>Proyectos</h1>
|
<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>
|
||||||
|
|
||||||
<div class="project-grid">
|
<div class="project-grid">
|
||||||
@@ -15,16 +20,24 @@
|
|||||||
<strong>Gestor:</strong> {{ project.manager.username|default:"No asignado" }}
|
<strong>Gestor:</strong> {{ project.manager.username|default:"No asignado" }}
|
||||||
</div>
|
</div>
|
||||||
<div class="project-stats">
|
<div class="project-stats">
|
||||||
<div style="display: flex; flex-direction: column;">
|
<div style="display: flex; flex-direction: column; gap: 0.5rem; width: 100%;">
|
||||||
<span style="font-size: 0.8rem; color: var(--text-secondary);">Gastos</span>
|
<div style="display: flex; justify-content: space-between;">
|
||||||
<span style="font-weight: 600; font-size: 1.1rem;">{{ project.expense_count|default:"0" }}</span>
|
<span style="font-size: 0.8rem; color: var(--efintel-gray);">Presupuesto Asignado</span>
|
||||||
</div>
|
<span style="font-weight: 700; font-family: var(--font-mono); color: var(--efintel-petroleum);">
|
||||||
<div style="display: flex; flex-direction: column; text-align: right;">
|
${{ project.assigned_budget_clp|floatformat:"0"|intcomma }}
|
||||||
<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>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
|||||||
@@ -1,71 +1,108 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="form-container">
|
<div class="form-container" style="max-width: 800px; margin: 0 auto;">
|
||||||
<div class="glass-card" style="padding: 2.5rem;">
|
<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);">
|
||||||
<h1 style="margin-bottom: 2rem;">
|
<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 %}
|
{% if edit_mode %}Editar Colaborador (Responsable): {{ target_user.username }}{% else %}Nuevo Colaborador (Responsable){% endif %}
|
||||||
</h1>
|
</h2>
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% 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">
|
||||||
<label for="{{ form.username.id_for_label }}">Nombre de Usuario</label>
|
<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 }}
|
{{ form.username }}
|
||||||
{% if form.username.errors %}
|
{% if form.username.errors %}
|
||||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.username.errors }}</div>
|
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.username.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.email.id_for_label }}">Correo Electrónico</label>
|
<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 }}
|
{{ form.email }}
|
||||||
{% if form.email.errors %}
|
{% if form.email.errors %}
|
||||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.email.errors }}</div>
|
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.email.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.password.id_for_label }}">
|
<label for="{{ form.password.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">
|
||||||
Contraseña
|
Contraseña
|
||||||
{% if edit_mode %}
|
{% if edit_mode %}
|
||||||
<span style="font-size: 0.8rem; color: var(--text-secondary);">(Opcional: Dejar en blanco para no cambiarla)</span>
|
<span style="font-size: 0.75rem; color: var(--efintel-gray); font-weight: 400;">(Opcional)</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</label>
|
</label>
|
||||||
{{ form.password }}
|
{{ form.password }}
|
||||||
{% if form.password.errors %}
|
{% if form.password.errors %}
|
||||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.password.errors }}</div>
|
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.password.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.role.id_for_label }}">Rol / Grupo del Sistema</label>
|
<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 }}
|
{{ form.role }}
|
||||||
{% if form.role.errors %}
|
{% if form.role.errors %}
|
||||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.role.errors }}</div>
|
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.role.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group" style="margin-bottom: 2rem;">
|
||||||
<label for="{{ form.description.id_for_label }}">Descripción / Bio del Responsable</label>
|
<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 }}
|
{{ form.description }}
|
||||||
{% if form.description.errors %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
|
<div class="form-group" style="margin-bottom: 2rem;">
|
||||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
<label style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Especialidades Técnicas (Categorías Asociadas)</label>
|
||||||
<a href="{% url 'gestion_panel' %}" class="btn btn-outline">Cancelar</a>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Apply form-control class to all inputs
|
// Apply form-control class to all inputs except checkboxes
|
||||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
document.querySelectorAll('input:not([type="checkbox"]), select, textarea').forEach(el => {
|
||||||
el.classList.add('form-control');
|
el.classList.add('form-control');
|
||||||
});
|
});
|
||||||
</script>
|
</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