mirror of
https://github.com/adrigongv23/G26---Telemetry-Software.git
synced 2026-08-25 11:33:17 +02:00
Delete Plataform_Web directory
This commit is contained in:
parent
4b58b1ec97
commit
5f81e54793
78 changed files with 0 additions and 3709 deletions
|
|
@ -1,39 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from .models import Documento, Factura
|
||||
|
||||
@admin.register(Documento)
|
||||
class DocumentoAdmin(admin.ModelAdmin):
|
||||
# Columnas principales
|
||||
list_display = ('nombre', 'categoria', 'tipo', 'subido_por', 'fecha_subida')
|
||||
|
||||
# Filtros por área técnica, tipo de documento y temporada del documento
|
||||
list_filter = ('categoria', 'tipo', 'temporada')
|
||||
|
||||
# Buscador de documentos por su nombre o descripción
|
||||
search_fields = ('nombre', 'descripcion')
|
||||
|
||||
# Para asignar a que temporada pertenece el documento
|
||||
filter_horizontal = ('temporada',)
|
||||
|
||||
# Automatización: para rellenar el creador del nuevo documento de manera automática
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not obj.subido_por:
|
||||
obj.subido_por = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
@admin.register(Factura)
|
||||
class FacturaAdmin(admin.ModelAdmin):
|
||||
# Herncia de Documento
|
||||
list_display = ('nombre', 'empresa', 'importe', 'categoria', 'subido_por', 'fecha_subida')
|
||||
|
||||
list_filter = ('categoria', 'temporada')
|
||||
search_fields = ('nombre', 'empresa', 'descripcion')
|
||||
|
||||
filter_horizontal = ('temporada',)
|
||||
|
||||
# Mantenemos la misma automatización
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not obj.subido_por:
|
||||
obj.subido_por = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DocumentosConfig(AppConfig):
|
||||
name = 'documentos'
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-02-17 11:36
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('temporadas', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Documento',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('titulo', models.CharField(max_length=100, verbose_name='Título del documento')),
|
||||
('archivo', models.FileField(upload_to='ingenieria_docs/')),
|
||||
('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('suspension', 'Suspensión'), ('motor', 'Motor/Powertrain'), ('electronica', 'Electrónica'), ('general', 'General / Normativa')], max_length=20)),
|
||||
('tipo', models.CharField(choices=[('diseno', 'Diseño / CAD'), ('simulacion', 'Simulación'), ('informe', 'Informe Técnico'), ('factura', 'Factura / Presupuesto'), ('otro', 'Otro')], default='informe', max_length=20)),
|
||||
('descripcion', models.TextField(blank=True, null=True, verbose_name='Descripción o notas')),
|
||||
('fecha_subida', models.DateTimeField(auto_now_add=True)),
|
||||
('subido_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documentos_subidos', to=settings.AUTH_USER_MODEL)),
|
||||
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Documento Técnico',
|
||||
'verbose_name_plural': 'Documentos de Ingeniería',
|
||||
'ordering': ['-fecha_subida'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-03-06 19:29
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('documentos', '0001_initial'),
|
||||
('temporadas', '0002_temporada_miembros'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Factura',
|
||||
fields=[
|
||||
('documento_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='documentos.documento')),
|
||||
('empresa', models.CharField(max_length=100, verbose_name='Nombre de la empresa')),
|
||||
('importe', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Importe (€)')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Factura',
|
||||
'verbose_name_plural': 'Facturas',
|
||||
},
|
||||
bases=('documentos.documento',),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='documento',
|
||||
options={'ordering': ['-fecha_subida'], 'verbose_name': 'Documento', 'verbose_name_plural': 'Documentos'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='documento',
|
||||
name='titulo',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='documento',
|
||||
name='nombre',
|
||||
field=models.CharField(default='Documento antiguo', max_length=100, verbose_name='Nombre del documento'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='documento',
|
||||
name='categoria',
|
||||
field=models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('e_powertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('normativa', 'General / Normativa')], max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='documento',
|
||||
name='descripcion',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Descripción del documento'),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='documento',
|
||||
name='temporada',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='documento',
|
||||
name='tipo',
|
||||
field=models.CharField(choices=[('fabricacion', 'Fabricación'), ('diseno', 'Diseño / CAD'), ('concepto', 'Concepto'), ('simulacion', 'Simulación'), ('dossier_patrocinado', 'Dossier Patrocinio'), ('informe', 'Informe'), ('tutorial', 'Tutorial'), ('otro', 'Otro')], default='informe', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='documento',
|
||||
name='temporada',
|
||||
field=models.ManyToManyField(related_name='documentos_asociados', to='temporadas.temporada'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-06-30 16:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('documentos', '0002_factura_alter_documento_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='factura',
|
||||
name='estado',
|
||||
field=models.CharField(choices=[('pendiente', 'Pendiente'), ('aceptada', 'Aceptada'), ('rechazada', 'Rechazada')], default='pendiente', max_length=20),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
from django.db import models
|
||||
from users.models import CustomUser
|
||||
from temporadas.models import Temporada
|
||||
import os
|
||||
|
||||
class Documento(models.Model):
|
||||
# Opciones de categorías (puedes añadir más)
|
||||
CATEGORIAS = (
|
||||
('aerodinamica', 'Aerodinámica'),
|
||||
('chasis', 'Chasis'),
|
||||
('business', 'Business & Operations'),
|
||||
('e_powertrain', 'E-Powertrain'),
|
||||
('electronica', 'Electrónica'),
|
||||
('sdf', 'SDF'),
|
||||
('motor_transmision', 'Motor & Transmisión'),
|
||||
('software', 'Software'),
|
||||
('normativa', 'General / Normativa'),
|
||||
)
|
||||
|
||||
TIPO_DOC = (
|
||||
('fabricacion', 'Fabricación'),
|
||||
('diseno', 'Diseño / CAD'),
|
||||
('concepto', 'Concepto'),
|
||||
('simulacion', 'Simulación'),
|
||||
('dossier_patrocinado', 'Dossier Patrocinio'),
|
||||
('informe', 'Informe'),
|
||||
('tutorial', 'Tutorial'),
|
||||
('otro', 'Otro'),
|
||||
)
|
||||
|
||||
# CAMPOS
|
||||
nombre = models.CharField(max_length=100, verbose_name="Nombre del documento")
|
||||
|
||||
# Aquí definimos dónde se guardan los archivos.
|
||||
# upload_to='ingenieria/' creará esa carpeta automáticamente.
|
||||
archivo = models.FileField(upload_to='ingenieria_docs/')
|
||||
|
||||
categoria = models.CharField(max_length=20, choices=CATEGORIAS)
|
||||
tipo = models.CharField(max_length=20, choices=TIPO_DOC, default='informe')
|
||||
descripcion = models.TextField(blank=True, null=True, verbose_name="Descripción del documento")
|
||||
fecha_subida = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# RELACIONES
|
||||
# Relacion Documento - Temporadas (Relación 1..* a 1..* )
|
||||
temporada = models.ManyToManyField(Temporada, related_name="documentos_asociados")
|
||||
|
||||
# 2. Relación 1 a N con el Usuario (Miembros)
|
||||
subido_por = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, related_name="documentos_subidos")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Documento"
|
||||
verbose_name_plural = "Documentos"
|
||||
ordering = ['-fecha_subida']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.nombre} ({self.get_categoria_display()})"
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# Esto borra el archivo físico del disco duro cuando borras la entrada en la base de datos
|
||||
if self.archivo:
|
||||
if os.path.isfile(self.archivo.path):
|
||||
os.remove(self.archivo.path)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
ESTADO_FACTURA = [
|
||||
('pendiente', 'Pendiente'),
|
||||
('aceptada', 'Aceptada'),
|
||||
('rechazada', 'Rechazada'),
|
||||
]
|
||||
|
||||
# Herencia de la clase Factura: Factura hereda de Documento
|
||||
class Factura(Documento):
|
||||
# Al heredar de Documento, ya tiene nombre, archivo, categoria, etc.
|
||||
empresa = models.CharField(max_length=100, verbose_name="Nombre de la empresa")
|
||||
importe = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Importe (€)")
|
||||
estado = models.CharField(max_length=20, choices=ESTADO_FACTURA, default='pendiente')
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Factura"
|
||||
verbose_name_plural = "Facturas"
|
||||
|
||||
def __str__(self):
|
||||
return f"Factura {self.empresa} - {self.importe}€"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
ASGI config for gades_manager project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gades_manager.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
"""
|
||||
Django settings for gades_manager project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 6.0.2.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-mwlyo^0ax9u9ts5(hw(e&vlgmr$nbla-3az0d%s(lymmp5^)$-'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'users',
|
||||
'temporadas',
|
||||
'documentos',
|
||||
'gestion',
|
||||
'pruebas',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'gades_manager.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / 'templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'gades_manager.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'formula_gades_bd', # El nombre exacto que pusiste en Workbench
|
||||
'USER': 'root',
|
||||
'PASSWORD': 'admin1234', # ¡La que apuntaste en el papel!
|
||||
'HOST': 'localhost', # Significa "este ordenador"
|
||||
'PORT': '3306',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / "static",
|
||||
]
|
||||
|
||||
AUTH_USER_MODEL = 'users.CustomUser'
|
||||
|
||||
MEDIA_URL = '/media/' # La URL pública (ej: tudominio.com/media/archivo.pdf)
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # La carpeta física en tu ordenador
|
||||
|
||||
LOGIN_URL = 'login'
|
||||
LOGIN_REDIRECT_URL = 'inicio'
|
||||
LOGOUT_REDIRECT_URL = 'login'
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
"""
|
||||
URL configuration for gades_manager project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/6.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.contrib.auth import views as auth_views
|
||||
from gestion import views
|
||||
from users import views as users_views
|
||||
from temporadas import views as temporadas_views
|
||||
from pruebas import views as pruebas_views
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', views.inicio, name='inicio'),
|
||||
path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'),
|
||||
path('logout/', auth_views.LogoutView.as_view(next_page='login'), name='logout'),
|
||||
path('mi-perfil/', users_views.mi_perfil, name='mi_perfil'),
|
||||
path('miembros/', users_views.listado_miembros, name='listado_miembros'),
|
||||
path('gestion/usuarios/', users_views.gestion_usuarios, name='gestion_usuarios'),
|
||||
path('gestion/usuarios/<int:pk>/editar/', users_views.editar_usuario, name='editar_usuario'),
|
||||
path('gestion/usuarios/<int:pk>/eliminar/', users_views.eliminar_usuario, name='eliminar_usuario'),
|
||||
path('gestion/temporadas/', temporadas_views.gestion_temporadas, name='gestion_temporadas'),
|
||||
path('gestion/temporadas/crear/', temporadas_views.crear_temporada, name='crear_temporada'),
|
||||
path('gestion/temporadas/<int:pk>/editar/', temporadas_views.editar_temporada, name='editar_temporada'),
|
||||
path('gestion/temporadas/<int:pk>/eliminar/', temporadas_views.eliminar_temporada, name='eliminar_temporada'),
|
||||
path('gestion/contabilidad/', views.contabilidad, name='contabilidad'),
|
||||
path('gestion/contabilidad/gasto/anadir/', views.anadir_gasto, name='anadir_gasto'),
|
||||
path('gestion/contabilidad/ingreso/anadir/', views.anadir_ingreso, name='anadir_ingreso'),
|
||||
path('gestion/contabilidad/factura/<int:pk>/aceptar/', views.aceptar_factura, name='aceptar_factura'),
|
||||
path('gestion/contabilidad/factura/<int:pk>/rechazar/', views.rechazar_factura, name='rechazar_factura'),
|
||||
path('patrocinios/', views.patrocinios, name='patrocinios'),
|
||||
path('patrocinios/proponer/', views.proponer_patrocinio, name='proponer_patrocinio'),
|
||||
path('patrocinios/<int:pk>/editar/', views.editar_patrocinio, name='editar_patrocinio'),
|
||||
path('patrocinios/<int:pk>/estado/', views.cambiar_estado_patrocinio, name='cambiar_estado_patrocinio'),
|
||||
path('pruebas/', pruebas_views.listado_pruebas, name='listado_pruebas'),
|
||||
path('pruebas/nueva/', pruebas_views.crear_prueba, name='crear_prueba'),
|
||||
path('pruebas/<int:pk>/', pruebas_views.detalle_prueba, name='detalle_prueba'),
|
||||
path('pruebas/<int:pk>/editar/', pruebas_views.editar_prueba, name='editar_prueba'),
|
||||
path('pruebas/<int:pk>/eliminar/', pruebas_views.eliminar_prueba, name='eliminar_prueba'),
|
||||
path('pruebas/<int:pk>/csv/subir/', pruebas_views.subir_csv, name='subir_csv'),
|
||||
]
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
WSGI config for gades_manager project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gades_manager.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from .models import Patrocinio, Pieza, Gasto, Ingreso
|
||||
|
||||
# INLINES
|
||||
class PiezaInline(admin.TabularInline):
|
||||
model = Pieza
|
||||
extra = 1
|
||||
|
||||
# PANEL PARA ADMINISTRACIÓN
|
||||
@admin.register(Patrocinio)
|
||||
class PatrocinioAdmin(admin.ModelAdmin):
|
||||
list_display = ('empresa', 'estado', 'tipo_patrocinio', 'importe_economico', 'temporada', 'contacto_equipo')
|
||||
|
||||
# Para editar el estado y pasar de En contacto a Aceptado o Denagado desde la lista, sin tener que hacerlo manual
|
||||
list_editable = ('estado',)
|
||||
|
||||
list_filter = ('estado', 'tipo_patrocinio', 'temporada')
|
||||
search_fields = ('empresa', 'persona_contacto', 'email_contacto')
|
||||
|
||||
# Metemos las piezas en el patrocinio, por si hiciera falta
|
||||
inlines = [PiezaInline]
|
||||
|
||||
@admin.register(Pieza)
|
||||
class PiezaAdmin(admin.ModelAdmin):
|
||||
list_display = ('nombre', 'cantidad', 'patrocinio')
|
||||
search_fields = ('nombre', 'patrocinio__empresa')
|
||||
|
||||
# CONTABILIDAD (Gastos e Ingresos)
|
||||
@admin.register(Gasto)
|
||||
class GastoAdmin(admin.ModelAdmin):
|
||||
# Heredamos los campos de Contabilidad
|
||||
list_display = ('concepto', 'importe', 'categoria', 'fecha', 'temporada')
|
||||
list_filter = ('categoria', 'temporada')
|
||||
search_fields = ('concepto', 'observaciones')
|
||||
|
||||
# Barra de navegación por fecha de gasto
|
||||
date_hierarchy = 'fecha'
|
||||
|
||||
@admin.register(Ingreso)
|
||||
class IngresoAdmin(admin.ModelAdmin):
|
||||
list_display = ('concepto', 'importe', 'categoria', 'fecha', 'temporada')
|
||||
list_filter = ('categoria', 'temporada')
|
||||
search_fields = ('concepto', 'observaciones')
|
||||
|
||||
# Barra de navegación por fecha de ingreso
|
||||
date_hierarchy = 'fecha'
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GestionConfig(AppConfig):
|
||||
name = 'gestion'
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
from django import forms
|
||||
from .models import Gasto, Ingreso, Patrocinio
|
||||
|
||||
|
||||
class GastoForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Gasto
|
||||
fields = ['concepto', 'importe', 'categoria', 'observaciones']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs['class'] = 'form-control'
|
||||
|
||||
|
||||
class IngresoForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Ingreso
|
||||
fields = ['concepto', 'importe', 'categoria', 'observaciones']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs['class'] = 'form-control'
|
||||
|
||||
|
||||
class PatrocinioForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Patrocinio
|
||||
fields = ['empresa', 'email_contacto', 'persona_contacto', 'tipo_patrocinio']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs['class'] = 'form-control'
|
||||
|
||||
|
||||
class PatrocinioEditForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Patrocinio
|
||||
fields = ['empresa', 'email_contacto', 'persona_contacto', 'tipo_patrocinio', 'estado', 'importe_economico']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs['class'] = 'form-control'
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-02-19 10:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('temporadas', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Factura',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nombre', models.CharField(max_length=100, verbose_name='Concepto de la factura')),
|
||||
('empresa', models.CharField(max_length=100, verbose_name='Empresa / Proveedor')),
|
||||
('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('general', 'General')], max_length=30)),
|
||||
('descripcion', models.TextField(blank=True, null=True, verbose_name='Breve descripción')),
|
||||
('archivo', models.FileField(upload_to='facturas/', verbose_name='Foto o PDF de la factura')),
|
||||
('subido_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Usuario que la sube')),
|
||||
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Patrocinador',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('empresa', models.CharField(max_length=100, verbose_name='Nombre de la Empresa')),
|
||||
('email_contacto', models.EmailField(max_length=254, verbose_name='Correo de contacto')),
|
||||
('tipo_empresa', models.CharField(max_length=30, verbose_name='Tipo de la Empresa')),
|
||||
('tipo_patrocinio', models.CharField(choices=[('economico', 'Económico'), ('piezas/materiales', 'Piezas/Materiales'), ('mixto', 'Mixto (Dinero y Piezas)')], max_length=30)),
|
||||
('detalle_piezas', models.TextField(blank=True, help_text='Detallar qué se ofrece si es en especies', null=True)),
|
||||
('estado', models.CharField(choices=[('en contacto', 'En contacto'), ('aceptado', 'Aceptado / Activo'), ('denegado', 'Denegado')], default='contacto', max_length=20)),
|
||||
('contacto_equipo', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Miembro al cargo')),
|
||||
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-03-06 19:29
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gestion', '0001_initial'),
|
||||
('temporadas', '0002_temporada_miembros'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='patrocinador',
|
||||
name='contacto_equipo',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='patrocinador',
|
||||
name='temporada',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Gasto',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('concepto', models.CharField(max_length=150)),
|
||||
('importe', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('fecha', models.DateField()),
|
||||
('observaciones', models.TextField(blank=True, null=True)),
|
||||
('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('e_powertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('general', 'General')], max_length=30, verbose_name='Área del gasto')),
|
||||
('doc_justificativo', models.FileField(blank=True, null=True, upload_to='contabilidad/gastos/', verbose_name='Ticket o Factura (PDF/IMG)')),
|
||||
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='%(class)s_registrados', to='temporadas.temporada')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Gasto',
|
||||
'verbose_name_plural': 'Gastos',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Ingreso',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('concepto', models.CharField(max_length=150)),
|
||||
('importe', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('fecha', models.DateField()),
|
||||
('observaciones', models.TextField(blank=True, null=True)),
|
||||
('categoria', models.CharField(choices=[('patrocinador', 'Patrocinador'), ('donación', 'Donación'), ('premio', 'Premio'), ('recaudación', 'Recaudación'), ('Cuotas', 'Cuotas'), ('Ventas', 'ventas')], max_length=30, verbose_name='Categoria del Ingreso')),
|
||||
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='%(class)s_registrados', to='temporadas.temporada')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Ingreso',
|
||||
'verbose_name_plural': 'Ingresos',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Patrocinio',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('empresa', models.CharField(max_length=100, verbose_name='Nombre de la Empresa')),
|
||||
('persona_contacto', models.CharField(blank=True, max_length=100, null=True, verbose_name='Persona de contacto')),
|
||||
('email_contacto', models.EmailField(max_length=254, verbose_name='Correo de contacto')),
|
||||
('tipo_patrocinio', models.CharField(choices=[('economico', 'Económico'), ('piezas/materiales', 'Piezas/Materiales'), ('mixto', 'Mixto (Dinero y Piezas)')], max_length=30)),
|
||||
('estado', models.CharField(choices=[('en_contacto', 'En contacto'), ('aceptado', 'Aceptado / Activo'), ('denegado', 'Denegado')], default='en_contacto', max_length=20)),
|
||||
('importe_economico', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='Importe (€)')),
|
||||
('fecha_contacto', models.DateField(auto_now_add=True)),
|
||||
('contacto_equipo', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patrocinios_gestionados', to=settings.AUTH_USER_MODEL, verbose_name='Persona al cargo del Patrocinio')),
|
||||
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='patrocinios', to='temporadas.temporada', verbose_name='Temporada del patrocinio')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Patrocinio',
|
||||
'verbose_name_plural': 'Patrocinios',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Pieza',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nombre', models.CharField(max_length=100, verbose_name='Nombre de la pieza/material')),
|
||||
('cantidad', models.PositiveIntegerField(default=1)),
|
||||
('patrocinio', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='piezas', to='gestion.patrocinio')),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Factura',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Patrocinador',
|
||||
),
|
||||
]
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from temporadas.models import Temporada
|
||||
|
||||
|
||||
class Patrocinio(models.Model):
|
||||
|
||||
TIPO_PATROCINIO = (
|
||||
('economico', 'Económico'),
|
||||
('piezas/materiales', 'Piezas/Materiales'),
|
||||
('mixto', 'Mixto (Dinero y Piezas)'),
|
||||
)
|
||||
|
||||
ESTADOS = (
|
||||
('en_contacto', 'En contacto'),
|
||||
('aceptado', 'Aceptado / Activo'),
|
||||
('denegado', 'Denegado'),
|
||||
)
|
||||
|
||||
empresa = models.CharField(max_length=100, verbose_name="Nombre de la Empresa")
|
||||
persona_contacto = models.CharField(max_length=100, blank=True, null=True, verbose_name="Persona de contacto")
|
||||
email_contacto = models.EmailField(verbose_name="Correo de contacto")
|
||||
tipo_patrocinio = models.CharField(max_length=30, choices=TIPO_PATROCINIO)
|
||||
estado = models.CharField(max_length=20, choices=ESTADOS, default='en_contacto')
|
||||
|
||||
# Si el patrocinio es únicamente material, se quedará en 0. Si es económico o mixto, se rellena
|
||||
importe_economico = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name="Importe (€)")
|
||||
|
||||
fecha_contacto = models.DateField(auto_now_add=True)
|
||||
# Relaciones
|
||||
temporada = models.ForeignKey(Temporada, on_delete=models.CASCADE, related_name="patrocinios", verbose_name="Temporada del patrocinio")
|
||||
contacto_equipo = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="patrocinios_gestionados", verbose_name="Persona al cargo del Patrocinio")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Patrocinio"
|
||||
verbose_name_plural = "Patrocinios"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.empresa} ({self.get_estado_display()})"
|
||||
|
||||
class Pieza(models.Model):
|
||||
nombre = models.CharField(max_length=100, verbose_name="Nombre de la pieza/material")
|
||||
cantidad = models.PositiveIntegerField(default=1)
|
||||
|
||||
# COMPOSICIÓN ESTRICTA: Si se borra el Patrocinio, se borran las Piezas irremediablemente (CASCADE)
|
||||
patrocinio = models.ForeignKey(Patrocinio, on_delete=models.CASCADE, related_name="piezas")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.cantidad}x {self.nombre} (De: {self.patrocinio.empresa})"
|
||||
|
||||
class Contabilidad(models.Model):
|
||||
concepto = models.CharField(max_length=150)
|
||||
importe = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
fecha = models.DateField()
|
||||
observaciones = models.TextField(blank=True, null=True)
|
||||
|
||||
# Relación con Temporada. PROTECT evita que borres una temporada si tiene contabilidad asociada (por temas legales)
|
||||
temporada = models.ForeignKey(Temporada, on_delete=models.PROTECT, related_name="%(class)s_registrados")
|
||||
|
||||
class Meta:
|
||||
abstract = True #marcamos la clase como abstracta
|
||||
ordering = ['-fecha']
|
||||
|
||||
|
||||
class Gasto(Contabilidad):
|
||||
|
||||
CATEGORIAS_GASTOS = (
|
||||
('aerodinamica', 'Aerodinámica'),
|
||||
('chasis', 'Chasis'),
|
||||
('business', 'Business & Operations'),
|
||||
('e_powertrain', 'E-Powertrain'),
|
||||
('electronica', 'Electrónica'),
|
||||
('sdf', 'SDF'),
|
||||
('motor_transmision', 'Motor & Transmisión'),
|
||||
('software', 'Software'),
|
||||
('general', 'General'),
|
||||
)
|
||||
# Hereda concepto, importe, fecha, observaciones y temporada automáticamente
|
||||
categoria = models.CharField(max_length=30, choices=CATEGORIAS_GASTOS, verbose_name="Área del gasto")
|
||||
doc_justificativo = models.FileField(upload_to='contabilidad/gastos/', blank=True, null=True, verbose_name="Ticket o Factura (PDF/IMG)")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Gasto"
|
||||
verbose_name_plural = "Gastos"
|
||||
|
||||
def __str__(self):
|
||||
return f"[-{self.importe}€] {self.concepto} ({self.temporada})"
|
||||
|
||||
class Ingreso(Contabilidad):
|
||||
CATEGORIAS_INGRESO = (
|
||||
('patrocinador', 'Patrocinador'),
|
||||
('donación', 'Donación'),
|
||||
('premio', 'Premio'),
|
||||
('recaudación', 'Recaudación'),
|
||||
('Cuotas', 'Cuotas'),
|
||||
('Ventas', 'ventas'),
|
||||
)
|
||||
|
||||
# Hereda concepto, importe, fecha, observaciones y temporada automáticamente
|
||||
categoria = models.CharField(max_length=30, choices=CATEGORIAS_INGRESO, verbose_name="Categoria del Ingreso")
|
||||
class Meta:
|
||||
verbose_name = "Ingreso"
|
||||
verbose_name_plural = "Ingresos"
|
||||
|
||||
def __str__(self):
|
||||
return f"[+{self.importe}€] {self.concepto} ({self.temporada})"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.db.models import Sum
|
||||
from datetime import date
|
||||
|
||||
from temporadas.models import Temporada
|
||||
from documentos.models import Factura
|
||||
from users.decorators import require_rol
|
||||
from .models import Gasto, Ingreso, Patrocinio
|
||||
from .forms import GastoForm, IngresoForm, PatrocinioForm, PatrocinioEditForm
|
||||
|
||||
# Categorías válidas en Gasto (Documento usa 'normativa' en su lugar de 'general')
|
||||
_CATEGORIAS_GASTO_VALIDAS = {c[0] for c in Gasto.CATEGORIAS_GASTOS}
|
||||
|
||||
|
||||
@login_required
|
||||
def inicio(request):
|
||||
temporada_activa = Temporada.objects.filter(actual=True).first()
|
||||
return render(request, 'index.html', {'temporada_actual': temporada_activa})
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
def contabilidad(request):
|
||||
temporada_actual = Temporada.objects.filter(actual=True).first()
|
||||
|
||||
gastos = []
|
||||
ingresos = []
|
||||
facturas_pendientes = []
|
||||
total_gastos = 0
|
||||
total_ingresos = 0
|
||||
presupuesto_inicial = 0
|
||||
presupuesto_actual = 0
|
||||
|
||||
if temporada_actual:
|
||||
gastos = Gasto.objects.filter(temporada=temporada_actual)
|
||||
ingresos = Ingreso.objects.filter(temporada=temporada_actual)
|
||||
facturas_pendientes = Factura.objects.filter(
|
||||
temporada=temporada_actual, estado='pendiente'
|
||||
)
|
||||
|
||||
total_gastos = gastos.aggregate(total=Sum('importe'))['total'] or 0
|
||||
total_ingresos = ingresos.aggregate(total=Sum('importe'))['total'] or 0
|
||||
presupuesto_inicial = temporada_actual.presupuesto
|
||||
presupuesto_actual = presupuesto_inicial - total_gastos + total_ingresos
|
||||
|
||||
return render(request, 'contabilidad.html', {
|
||||
'temporada_actual': temporada_actual,
|
||||
'gastos': gastos,
|
||||
'ingresos': ingresos,
|
||||
'facturas_pendientes': facturas_pendientes,
|
||||
'total_gastos': total_gastos,
|
||||
'total_ingresos': total_ingresos,
|
||||
'presupuesto_inicial': presupuesto_inicial,
|
||||
'presupuesto_actual': presupuesto_actual,
|
||||
'gasto_form': GastoForm(),
|
||||
'ingreso_form': IngresoForm(),
|
||||
})
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
@require_POST
|
||||
def anadir_gasto(request):
|
||||
temporada_actual = Temporada.objects.filter(actual=True).first()
|
||||
if not temporada_actual:
|
||||
messages.error(request, 'No hay temporada activa.')
|
||||
return redirect('contabilidad')
|
||||
|
||||
form = GastoForm(request.POST)
|
||||
if form.is_valid():
|
||||
gasto = form.save(commit=False)
|
||||
gasto.fecha = date.today()
|
||||
gasto.temporada = temporada_actual
|
||||
gasto.save()
|
||||
messages.success(request, 'Gasto añadido correctamente.')
|
||||
else:
|
||||
messages.error(request, 'Error al añadir el gasto. Revisa los datos.')
|
||||
return redirect('contabilidad')
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
@require_POST
|
||||
def anadir_ingreso(request):
|
||||
temporada_actual = Temporada.objects.filter(actual=True).first()
|
||||
if not temporada_actual:
|
||||
messages.error(request, 'No hay temporada activa.')
|
||||
return redirect('contabilidad')
|
||||
|
||||
form = IngresoForm(request.POST)
|
||||
if form.is_valid():
|
||||
ingreso = form.save(commit=False)
|
||||
ingreso.fecha = date.today()
|
||||
ingreso.temporada = temporada_actual
|
||||
ingreso.save()
|
||||
messages.success(request, 'Ingreso añadido correctamente.')
|
||||
else:
|
||||
messages.error(request, 'Error al añadir el ingreso. Revisa los datos.')
|
||||
return redirect('contabilidad')
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
@require_POST
|
||||
def aceptar_factura(request, pk):
|
||||
factura = get_object_or_404(Factura, pk=pk)
|
||||
temporada_actual = Temporada.objects.filter(actual=True).first()
|
||||
if not temporada_actual:
|
||||
messages.error(request, 'No hay temporada activa.')
|
||||
return redirect('contabilidad')
|
||||
|
||||
categoria = factura.categoria if factura.categoria in _CATEGORIAS_GASTO_VALIDAS else 'general'
|
||||
Gasto.objects.create(
|
||||
concepto=factura.nombre,
|
||||
importe=factura.importe,
|
||||
fecha=date.today(),
|
||||
categoria=categoria,
|
||||
temporada=temporada_actual,
|
||||
observaciones=f'Factura de {factura.empresa}',
|
||||
doc_justificativo=factura.archivo,
|
||||
)
|
||||
factura.estado = 'aceptada'
|
||||
factura.save()
|
||||
messages.success(request, f'Factura de {factura.empresa} aceptada y registrada como gasto.')
|
||||
return redirect('contabilidad')
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
@require_POST
|
||||
def rechazar_factura(request, pk):
|
||||
factura = get_object_or_404(Factura, pk=pk)
|
||||
factura.estado = 'rechazada'
|
||||
factura.save()
|
||||
messages.success(request, f'Factura de {factura.empresa} rechazada.')
|
||||
return redirect('contabilidad')
|
||||
|
||||
|
||||
@login_required
|
||||
def patrocinios(request):
|
||||
temporada_actual = Temporada.objects.filter(actual=True).first()
|
||||
pendientes = []
|
||||
aceptados = []
|
||||
denegados = []
|
||||
if temporada_actual:
|
||||
qs = Patrocinio.objects.filter(temporada=temporada_actual).select_related('contacto_equipo')
|
||||
pendientes = qs.filter(estado='en_contacto')
|
||||
aceptados = qs.filter(estado='aceptado')
|
||||
denegados = qs.filter(estado='denegado')
|
||||
return render(request, 'patrocinios.html', {
|
||||
'temporada_actual': temporada_actual,
|
||||
'pendientes': pendientes,
|
||||
'aceptados': aceptados,
|
||||
'denegados': denegados,
|
||||
'form': PatrocinioForm(),
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def proponer_patrocinio(request):
|
||||
temporada_actual = Temporada.objects.filter(actual=True).first()
|
||||
if not temporada_actual:
|
||||
messages.error(request, 'No hay temporada activa. No se puede proponer un patrocinio.')
|
||||
return redirect('patrocinios')
|
||||
|
||||
form = PatrocinioForm(request.POST)
|
||||
if form.is_valid():
|
||||
empresa = form.cleaned_data['empresa'].strip()
|
||||
if Patrocinio.objects.filter(empresa__iexact=empresa, temporada=temporada_actual).exists():
|
||||
messages.error(request, f'Ya existe un patrocinio con "{empresa}" en la temporada actual.')
|
||||
else:
|
||||
patrocinio = form.save(commit=False)
|
||||
patrocinio.estado = 'en_contacto'
|
||||
patrocinio.temporada = temporada_actual
|
||||
patrocinio.contacto_equipo = request.user
|
||||
patrocinio.save()
|
||||
messages.success(request, f'Patrocinio de "{empresa}" propuesto correctamente.')
|
||||
else:
|
||||
messages.error(request, 'Error en el formulario. Revisa los datos.')
|
||||
return redirect('patrocinios')
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
def editar_patrocinio(request, pk):
|
||||
patrocinio = get_object_or_404(Patrocinio, pk=pk)
|
||||
form = PatrocinioEditForm(request.POST or None, instance=patrocinio)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Patrocinio actualizado correctamente.')
|
||||
return redirect('patrocinios')
|
||||
return render(request, 'editar_patrocinio.html', {'form': form, 'patrocinio': patrocinio})
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
@require_POST
|
||||
def cambiar_estado_patrocinio(request, pk):
|
||||
patrocinio = get_object_or_404(Patrocinio, pk=pk)
|
||||
nuevo_estado = request.POST.get('estado')
|
||||
if nuevo_estado in ('aceptado', 'denegado', 'en_contacto'):
|
||||
patrocinio.estado = nuevo_estado
|
||||
patrocinio.save()
|
||||
messages.success(request, f'Estado de "{patrocinio.empresa}" actualizado.')
|
||||
return redirect('patrocinios')
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gades_manager.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 118 KiB |
|
|
@ -1,50 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from .models import Prueba, Telemetria, Variable
|
||||
|
||||
# INLINES
|
||||
# Usamos inlines que nos sirve para cuando se este creando un registro de Telemetría, podremos añadir sus Variables en la misma pantalla sin tener que ir a otro menú
|
||||
class VariableInline(admin.TabularInline):
|
||||
model = Variable
|
||||
extra = 1
|
||||
|
||||
class TelemetriaInline(admin.TabularInline):
|
||||
model = Telemetria
|
||||
extra = 0
|
||||
|
||||
# PANELES DE ADMINISTRACIÓN
|
||||
@admin.register(Prueba)
|
||||
class PruebaAdmin(admin.ModelAdmin):
|
||||
list_display = ('nombre', 'categoria', 'fecha_inicio', 'temporada', 'realizado_por')
|
||||
|
||||
# Filtros para encontrar los test dado una temporada y una área técnica
|
||||
list_filter = ('categoria', 'temporada')
|
||||
|
||||
# Buscador de texto
|
||||
search_fields = ('nombre', 'descripcion', 'resultados')
|
||||
|
||||
# Una barra de navegación en la parte superior, para poder hacer clic en un año y en un mes y mostrar todos los datos de ese me
|
||||
date_hierarchy = 'fecha_inicio'
|
||||
|
||||
# Mostramos los archivos de telemetría directamente dentro del test
|
||||
inlines = [TelemetriaInline]
|
||||
|
||||
|
||||
@admin.register(Telemetria)
|
||||
class TelemetriaAdmin(admin.ModelAdmin):
|
||||
list_display = ('nombre', 'prueba', 'fecha_subida')
|
||||
|
||||
# Filtra test de telemetría basandose en la temporada o área tecnica de su test correspondiente
|
||||
list_filter = ('prueba__temporada', 'prueba__categoria')
|
||||
|
||||
# Buscador del nombre de la telemetría o por el nombre de la prueba
|
||||
search_fields = ('nombre', 'prueba__nombre')
|
||||
|
||||
# Mostramos las variables de su telemetría
|
||||
inlines = [VariableInline]
|
||||
|
||||
|
||||
@admin.register(Variable)
|
||||
class VariableAdmin(admin.ModelAdmin):
|
||||
# Aunque se pueden crear desde el Inline, dejamos su tabla propia por si fuese necesario, por prevenir
|
||||
list_display = ('nombre', 'unidad_medida', 'telemetria')
|
||||
search_fields = ('nombre', 'telemetria__nombre')
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PruebasConfig(AppConfig):
|
||||
name = 'pruebas'
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
from django import forms
|
||||
from .models import Prueba, Telemetria
|
||||
|
||||
|
||||
class PruebaForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Prueba
|
||||
fields = ['nombre', 'descripcion', 'fecha_inicio', 'fecha_fin', 'categoria', 'resultados', 'temporada']
|
||||
widgets = {
|
||||
'fecha_inicio': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
'fecha_fin': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
}
|
||||
|
||||
|
||||
class TelemetriaForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Telemetria
|
||||
fields = ['nombre', 'archivo_csv']
|
||||
widgets = {
|
||||
'nombre': forms.TextInput(attrs={'class': 'form-control form-control-sm'}),
|
||||
'archivo_csv': forms.ClearableFileInput(attrs={'class': 'form-control form-control-sm'}),
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-02-19 10:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('temporadas', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TestGeneral',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nombre', models.CharField(max_length=150, verbose_name='Nombre del Test')),
|
||||
('descripcion', models.TextField(verbose_name='Objetivo / Descripción del test')),
|
||||
('fecha_inicio', models.DateField()),
|
||||
('fecha_fin', models.DateField()),
|
||||
('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('general', 'General')], default='general', max_length=30)),
|
||||
('resultados', models.TextField(blank=True, null=True, verbose_name='Conclusiones y Resultados')),
|
||||
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Test General',
|
||||
'verbose_name_plural': 'Tests Generales',
|
||||
'ordering': ['-fecha_inicio'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-03-06 19:29
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pruebas', '0001_initial'),
|
||||
('temporadas', '0002_temporada_miembros'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Prueba',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nombre', models.CharField(max_length=150, verbose_name='Nombre del Test')),
|
||||
('descripcion', models.TextField(verbose_name='Objetivo / Descripción de la prueba')),
|
||||
('fecha_inicio', models.DateField()),
|
||||
('fecha_fin', models.DateField()),
|
||||
('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('general', 'General')], default='general', max_length=30)),
|
||||
('resultados', models.TextField(blank=True, null=True, verbose_name='Conclusiones y Resultados')),
|
||||
('realizado_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pruebas_realizadas', to=settings.AUTH_USER_MODEL, verbose_name='Realizado por')),
|
||||
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pruebas', to='temporadas.temporada')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Prueba',
|
||||
'verbose_name_plural': 'Pruebas',
|
||||
'ordering': ['-fecha_inicio'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Telemetria',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nombre', models.CharField(max_length=100, verbose_name='Nombre del registro')),
|
||||
('archivo_csv', models.FileField(upload_to='telemetria/archivos_csv/', verbose_name='Archivo de Datos (CSV)')),
|
||||
('fecha_subida', models.DateTimeField(auto_now_add=True)),
|
||||
('prueba', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='archivos_telemetria', to='pruebas.prueba')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Registro de Telemetría',
|
||||
'verbose_name_plural': 'Registros de Telemetría',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Variable',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nombre', models.CharField(max_length=50, verbose_name='Nombre ')),
|
||||
('unidad_medida', models.CharField(blank=True, max_length=20, null=True, verbose_name='Unidad de medida')),
|
||||
('descripcion', models.TextField(blank=True, null=True)),
|
||||
('telemetria', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variables', to='pruebas.telemetria')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Variable de Telemetría',
|
||||
'verbose_name_plural': 'Variables de Telemetría',
|
||||
},
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='TestGeneral',
|
||||
),
|
||||
]
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from temporadas.models import Temporada
|
||||
import os
|
||||
|
||||
class Prueba(models.Model):
|
||||
CATEGORIAS_TEST = (
|
||||
('aerodinamica', 'Aerodinámica'),
|
||||
('chasis', 'Chasis'),
|
||||
('epowertrain', 'E-Powertrain'),
|
||||
('electronica', 'Electrónica'),
|
||||
('sdf', 'SDF'),
|
||||
('motor_transmision', 'Motor & Transmisión'),
|
||||
('software', 'Software'),
|
||||
('general', 'General'),
|
||||
)
|
||||
|
||||
nombre = models.CharField(max_length=150, verbose_name="Nombre del Test")
|
||||
descripcion = models.TextField(verbose_name="Objetivo / Descripción de la prueba")
|
||||
|
||||
fecha_inicio = models.DateField()
|
||||
fecha_fin = models.DateField()
|
||||
|
||||
categoria = models.CharField(max_length=30, choices=CATEGORIAS_TEST, default='general')
|
||||
resultados = models.TextField(blank=True, null=True, verbose_name="Conclusiones y Resultados")
|
||||
|
||||
# Relaciones
|
||||
# Relacion con Temporada
|
||||
temporada = models.ForeignKey(Temporada, on_delete=models.CASCADE, related_name="pruebas")
|
||||
#Relacion con Usuario
|
||||
realizado_por = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="pruebas_realizadas", verbose_name="Realizado por")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Prueba"
|
||||
verbose_name_plural = "Pruebas"
|
||||
ordering = ['-fecha_inicio']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.nombre} ({self.fecha_inicio})"
|
||||
|
||||
class Telemetria(models.Model):
|
||||
nombre = models.CharField(max_length=100, verbose_name="Nombre del registro")
|
||||
archivo_csv = models.FileField(upload_to='telemetria/archivos_csv/', verbose_name="Archivo de Datos (CSV)")
|
||||
fecha_subida = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# La telemetría se obtiene durante una Prueba en pista
|
||||
prueba = models.ForeignKey(Prueba, on_delete=models.CASCADE, related_name="archivos_telemetria")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Registro de Telemetría"
|
||||
verbose_name_plural = "Registros de Telemetría"
|
||||
|
||||
def __str__(self):
|
||||
return f"Telemetría: {self.nombre} (De: {self.prueba.nombre})"
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# Borra el archivo físico del disco duro cuando borras la entrada en la base de datos
|
||||
if self.archivo_csv:
|
||||
if os.path.isfile(self.archivo_csv.path):
|
||||
os.remove(self.archivo_csv.path)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
class Variable(models.Model):
|
||||
nombre = models.CharField(max_length=50, verbose_name="Nombre ")
|
||||
unidad_medida = models.CharField(max_length=20, verbose_name="Unidad de medida", blank=True, null=True)
|
||||
descripcion = models.TextField(blank=True, null=True)
|
||||
|
||||
# COMPOSICIÓN
|
||||
telemetria = models.ForeignKey(Telemetria, on_delete=models.CASCADE, related_name="variables")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Variable de Telemetría"
|
||||
verbose_name_plural = "Variables de Telemetría"
|
||||
|
||||
def __str__(self):
|
||||
unidad = f" [{self.unidad_medida}]" if self.unidad_medida else ""
|
||||
return f"{self.nombre}{unidad} (De: {self.telemetria.nombre})"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.http import require_POST
|
||||
from users.decorators import require_rol
|
||||
from temporadas.models import Temporada
|
||||
from .models import Prueba
|
||||
from .forms import PruebaForm, TelemetriaForm
|
||||
|
||||
|
||||
def puede_subir_csv(user):
|
||||
return user.rol == 'directiva' or user.especialidad == 'software'
|
||||
|
||||
|
||||
@login_required
|
||||
def listado_pruebas(request):
|
||||
temporada_actual = Temporada.objects.filter(actual=True).first()
|
||||
temporada_id = request.GET.get('temporada') or (temporada_actual.pk if temporada_actual else None)
|
||||
categoria = request.GET.get('categoria')
|
||||
|
||||
pruebas = Prueba.objects.all()
|
||||
if temporada_id:
|
||||
pruebas = pruebas.filter(temporada_id=temporada_id)
|
||||
if categoria:
|
||||
pruebas = pruebas.filter(categoria=categoria)
|
||||
|
||||
return render(request, 'listado_pruebas.html', {
|
||||
'pruebas': pruebas,
|
||||
'temporadas': Temporada.objects.all(),
|
||||
'categorias': Prueba.CATEGORIAS_TEST,
|
||||
'temporada_seleccionada': str(temporada_id) if temporada_id else '',
|
||||
'categoria_seleccionada': categoria or '',
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def detalle_prueba(request, pk):
|
||||
prueba = get_object_or_404(Prueba, pk=pk)
|
||||
puede_editar = request.user.rol == 'directiva' or (
|
||||
request.user.rol == 'jefe_area' and prueba.realizado_por_id == request.user.id
|
||||
)
|
||||
return render(request, 'detalle_prueba.html', {
|
||||
'prueba': prueba,
|
||||
'puede_editar': puede_editar,
|
||||
'puede_subir_csv': puede_subir_csv(request.user),
|
||||
'form_csv': TelemetriaForm(),
|
||||
})
|
||||
|
||||
|
||||
@require_rol('directiva', 'jefe_area')
|
||||
def crear_prueba(request):
|
||||
form = PruebaForm(request.POST or None)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
prueba = form.save(commit=False)
|
||||
prueba.realizado_por = request.user
|
||||
prueba.save()
|
||||
messages.success(request, 'Test creado correctamente.')
|
||||
return redirect('listado_pruebas')
|
||||
return render(request, 'editar_prueba.html', {'form': form, 'prueba': None})
|
||||
|
||||
|
||||
@require_rol('directiva', 'jefe_area')
|
||||
def editar_prueba(request, pk):
|
||||
prueba = get_object_or_404(Prueba, pk=pk)
|
||||
if request.user.rol == 'jefe_area' and prueba.realizado_por_id != request.user.id:
|
||||
messages.error(request, 'Solo puedes editar los tests que tú has creado.')
|
||||
return redirect('detalle_prueba', pk=prueba.pk)
|
||||
|
||||
form = PruebaForm(request.POST or None, instance=prueba)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Test actualizado correctamente.')
|
||||
return redirect('detalle_prueba', pk=prueba.pk)
|
||||
return render(request, 'editar_prueba.html', {'form': form, 'prueba': prueba})
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
@require_POST
|
||||
def eliminar_prueba(request, pk):
|
||||
prueba = get_object_or_404(Prueba, pk=pk)
|
||||
prueba.delete()
|
||||
messages.success(request, f'Test "{prueba.nombre}" eliminado.')
|
||||
return redirect('listado_pruebas')
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def subir_csv(request, pk):
|
||||
prueba = get_object_or_404(Prueba, pk=pk)
|
||||
if not puede_subir_csv(request.user):
|
||||
messages.error(request, 'No tienes permiso para subir archivos de telemetría.')
|
||||
return redirect('detalle_prueba', pk=prueba.pk)
|
||||
|
||||
form = TelemetriaForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
telemetria = form.save(commit=False)
|
||||
telemetria.prueba = prueba
|
||||
telemetria.save()
|
||||
messages.success(request, 'Archivo de telemetría subido correctamente.')
|
||||
else:
|
||||
messages.error(request, 'No se pudo subir el archivo. Revisa el formulario.')
|
||||
return redirect('detalle_prueba', pk=prueba.pk)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.3 MiB |
|
|
@ -1,111 +0,0 @@
|
|||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Intranet | Formula Gades{% endblock title %}</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="{% static 'css/estilos.css' %}">
|
||||
|
||||
{% block extra_head %}{% endblock extra_head %}
|
||||
<style>
|
||||
html, body { overflow: hidden; height: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light text-dark d-flex flex-column min-vh-100">
|
||||
|
||||
<nav class="navbar navbar-expand-xl navbar-dark bg-dark sticky-top shadow-sm">
|
||||
<div class="container-fluid px-4">
|
||||
<a class="navbar-brand d-flex align-items-center" href="{% url 'inicio' %}">
|
||||
<img src="{% static 'images/logo_gades.png' %}" alt="Logo Gades" height="40" class="me-2">
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto align-items-center">
|
||||
|
||||
<li class="nav-item dropdown px-2">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="areasDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Áreas Técnicas
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="areasDropdown">
|
||||
<li><a class="dropdown-menu-item dropdown-item" href="#">Aerodinámica</a></li>
|
||||
<li><a class="dropdown-item" href="#">Chasis</a></li>
|
||||
<li><a class="dropdown-item" href="#">Bussiness & Operation</a></li>
|
||||
<li><a class="dropdown-item" href="#">E_Powertrain</a></li>
|
||||
<li><a class="dropdown-item" href="#">Electrónica</a></li>
|
||||
<li><a class="dropdown-item" href="#">SDF</a></li>
|
||||
<li><a class="dropdown-item" href="#">Motor & Transmisión</a></li>
|
||||
<li><a class="dropdown-item" href="#">Software</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-item px-2">
|
||||
<a class="nav-link" href="{% url 'listado_pruebas' %}">Pruebas</a>
|
||||
</li>
|
||||
<li class="nav-item px-2">
|
||||
<a class="nav-link" href="{% url 'patrocinios' %}">Patrocinios</a>
|
||||
</li>
|
||||
<li class="nav-item px-2">
|
||||
<a class="nav-link" href="{% url 'listado_miembros' %}">Miembros</a>
|
||||
</li>
|
||||
|
||||
{% if user.is_authenticated and user.rol == 'directiva' %}
|
||||
<li class="nav-item px-2">
|
||||
<a class="nav-link text-warning" href="{% url 'contabilidad' %}">Contabilidad</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown px-2">
|
||||
<a class="nav-link dropdown-toggle text-warning" href="#" id="gestionDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Gestión
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="gestionDropdown">
|
||||
<li><a class="dropdown-item" href="{% url 'gestion_usuarios' %}">Gestión de Usuarios</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'gestion_temporadas' %}">Gestión de Temporadas</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item dropdown ms-3 border-start ps-3">
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<div class="bg-secondary rounded-circle d-flex align-items-center justify-content-center text-white fw-bold shadow-sm" style="width: 35px; height: 35px;">
|
||||
{{ user.first_name|slice:":1"|upper }}
|
||||
</div>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
|
||||
<li><span class="dropdown-item-text fw-bold text-muted">Hola, {{ user.first_name }}</span></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{% url 'mi_perfil' %}">Mi Perfil</a></li>
|
||||
<li>
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="dropdown-item text-danger">Cerrar Sesión</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container-fluid py-3 px-4 flex-grow-1 d-flex flex-column" style="min-height: 0;">
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</main>
|
||||
|
||||
<footer class="bg-dark text-white text-center py-2 mt-auto border-top border-secondary">
|
||||
<p class="mb-0 small">© 2026 Formula Gades - Universidad de Cádiz. Todos los derechos reservados.</p>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
{% block extra_js %}{% endblock extra_js %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,247 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Contabilidad | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #ffc107;">
|
||||
<h1 class="display-6 fw-bold mb-1">Contabilidad</h1>
|
||||
<p class="text-white-50 mb-0">
|
||||
{% if temporada_actual %}{{ temporada_actual.nombre }}{% else %}Sin temporada activa{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if messages %}
|
||||
<div class="row mb-2 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %} py-2 small mb-1">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not temporada_actual %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">No hay ninguna temporada activa. Activa una temporada desde <a href="{% url 'gestion_temporadas' %}">Gestión de Temporadas</a>.</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
{# ── Bloque 1: 4 tarjetas resumen ── #}
|
||||
<div class="row g-3 mb-3 flex-shrink-0">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center h-100" style="border-top: 4px solid #0d6efd !important;">
|
||||
<div class="card-body py-3">
|
||||
<p class="text-secondary small mb-1">Presupuesto Inicial</p>
|
||||
<h4 class="fw-bold text-primary mb-0">{{ presupuesto_inicial|floatformat:2 }} €</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center h-100" style="border-top: 4px solid #198754 !important;">
|
||||
<div class="card-body py-3">
|
||||
<p class="text-secondary small mb-1">Total Ingresos</p>
|
||||
<h4 class="fw-bold text-success mb-0">+{{ total_ingresos|floatformat:2 }} €</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center h-100" style="border-top: 4px solid #dc3545 !important;">
|
||||
<div class="card-body py-3">
|
||||
<p class="text-secondary small mb-1">Total Gastos</p>
|
||||
<h4 class="fw-bold text-danger mb-0">-{{ total_gastos|floatformat:2 }} €</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center h-100"
|
||||
style="border-top: 4px solid {% if presupuesto_actual >= 0 %}#198754{% else %}#dc3545{% endif %} !important;">
|
||||
<div class="card-body py-3">
|
||||
<p class="text-secondary small mb-1">Presupuesto Actual</p>
|
||||
<h4 class="fw-bold {% if presupuesto_actual >= 0 %}text-success{% else %}text-danger{% endif %} mb-0">
|
||||
{{ presupuesto_actual|floatformat:2 }} €
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Bloque 2: Gastos | Ingresos ── #}
|
||||
<div class="row g-3 mb-3" style="overflow-y: auto; max-height: 340px;">
|
||||
|
||||
{# Columna Gastos #}
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-bold mb-0 text-danger">Gastos</h6>
|
||||
<button class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#modalGasto">+ Añadir</button>
|
||||
</div>
|
||||
<div style="max-height: 220px; overflow-y: auto;">
|
||||
{% if gastos %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for gasto in gastos %}
|
||||
<li class="list-group-item px-0 py-2 d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="fw-semibold small">{{ gasto.concepto }}</div>
|
||||
<div class="text-secondary" style="font-size:0.75rem;">{{ gasto.get_categoria_display }} · {{ gasto.fecha|date:"d/m/Y" }}</div>
|
||||
</div>
|
||||
<span class="badge bg-danger rounded-pill ms-2">-{{ gasto.importe|floatformat:2 }} €</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-secondary small text-center py-3 mb-0">Sin gastos registrados.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Columna Ingresos #}
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-bold mb-0 text-success">Ingresos</h6>
|
||||
<button class="btn btn-sm btn-outline-success" data-bs-toggle="modal" data-bs-target="#modalIngreso">+ Añadir</button>
|
||||
</div>
|
||||
<div style="max-height: 220px; overflow-y: auto;">
|
||||
{% if ingresos %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for ingreso in ingresos %}
|
||||
<li class="list-group-item px-0 py-2 d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="fw-semibold small">{{ ingreso.concepto }}</div>
|
||||
<div class="text-secondary" style="font-size:0.75rem;">{{ ingreso.get_categoria_display }} · {{ ingreso.fecha|date:"d/m/Y" }}</div>
|
||||
</div>
|
||||
<span class="badge bg-success rounded-pill ms-2">+{{ ingreso.importe|floatformat:2 }} €</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-secondary small text-center py-3 mb-0">Sin ingresos registrados.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Bloque 3: Facturas pendientes ── #}
|
||||
<div class="row flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<h6 class="fw-bold mb-3">Facturas Pendientes</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Empresa</th>
|
||||
<th>Importe</th>
|
||||
<th>Categoría</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for factura in facturas_pendientes %}
|
||||
<tr>
|
||||
<td>{{ factura.nombre }}</td>
|
||||
<td>{{ factura.empresa }}</td>
|
||||
<td class="fw-semibold">{{ factura.importe|floatformat:2 }} €</td>
|
||||
<td>{{ factura.get_categoria_display }}</td>
|
||||
<td class="d-flex gap-2">
|
||||
<form method="post" action="{% url 'aceptar_factura' factura.pk %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-success fw-bold">Aceptar</button>
|
||||
</form>
|
||||
<form method="post" action="{% url 'rechazar_factura' factura.pk %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">Rechazar</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-secondary py-3">No hay facturas pendientes.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}{# end if temporada_actual #}
|
||||
|
||||
{# ── Modal Añadir Gasto ── #}
|
||||
<div class="modal fade" id="modalGasto" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Añadir Gasto</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="post" action="{% url 'anadir_gasto' %}">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
{% for field in gasto_form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-danger fw-bold">Guardar Gasto</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Modal Añadir Ingreso ── #}
|
||||
<div class="modal fade" id="modalIngreso" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Añadir Ingreso</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="post" action="{% url 'anadir_ingreso' %}">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
{% for field in ingreso_form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-success fw-bold">Guardar Ingreso</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ prueba.nombre }} | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm d-flex justify-content-between align-items-center" style="border-left: 5px solid #0d6efd;">
|
||||
<div>
|
||||
<h1 class="display-6 fw-bold mb-1">{{ prueba.nombre }}</h1>
|
||||
<p class="text-white-50 mb-0">{{ prueba.get_categoria_display }} · Temporada {{ prueba.temporada.nombre }}</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if puede_editar %}
|
||||
<a href="{% url 'editar_prueba' prueba.pk %}" class="btn btn-outline-light fw-bold">Editar</a>
|
||||
{% endif %}
|
||||
{% if user.rol == 'directiva' %}
|
||||
<button type="button" class="btn btn-outline-danger fw-bold" data-bs-toggle="modal" data-bs-target="#eliminarModal">Eliminar</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4" style="overflow-y: auto;">
|
||||
|
||||
{% if messages %}
|
||||
<div class="col-12">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-success py-2 small mb-0">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-semibold">Objetivo / Descripción</h5>
|
||||
<p>{{ prueba.descripcion }}</p>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-6"><strong>Fecha inicio:</strong> {{ prueba.fecha_inicio|date:"d/m/Y" }}</div>
|
||||
<div class="col-6"><strong>Fecha fin:</strong> {{ prueba.fecha_fin|date:"d/m/Y" }}</div>
|
||||
</div>
|
||||
<p><strong>Realizado por:</strong> {{ prueba.realizado_por.first_name|default:"—" }}</p>
|
||||
|
||||
{% if prueba.resultados %}
|
||||
<h5 class="fw-semibold mt-4">Conclusiones y Resultados</h5>
|
||||
<p>{{ prueba.resultados }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-semibold mb-3">Archivos de telemetría</h5>
|
||||
<ul class="list-group list-group-flush mb-3">
|
||||
{% for telemetria in prueba.archivos_telemetria.all %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<a href="{{ telemetria.archivo_csv.url }}">{{ telemetria.nombre }}</a>
|
||||
<span class="text-secondary small">{{ telemetria.fecha_subida|date:"d/m/Y" }}</span>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="list-group-item px-0 text-secondary small">No hay archivos subidos todavía.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if puede_subir_csv %}
|
||||
<form method="post" action="{% url 'subir_csv' prueba.pk %}" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold">Nombre</label>
|
||||
{{ form_csv.nombre }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold">Archivo CSV</label>
|
||||
{{ form_csv.archivo_csv }}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-primary fw-bold">Subir CSV</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user.rol == 'directiva' %}
|
||||
<div class="modal fade" id="eliminarModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Confirmar eliminación</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
¿Seguro que quieres eliminar el test <strong>{{ prueba.nombre }}</strong>? Esta acción no se puede deshacer.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<form method="post" action="{% url 'eliminar_prueba' prueba.pk %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger fw-bold">Eliminar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Editar Patrocinio | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
|
||||
<h1 class="display-6 fw-bold mb-1">Editar Patrocinio</h1>
|
||||
<p class="text-white-50 mb-0">{{ patrocinio.empresa }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4" style="overflow-y: auto;">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="card-title fw-bold mb-3">Datos del Patrocinio</h5>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ field.id_for_label }}" class="form-label fw-semibold">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary fw-bold px-4">Guardar Cambios</button>
|
||||
<a href="{% url 'patrocinios' %}" class="btn btn-outline-secondary px-4">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% if prueba %}Editar Test{% else %}Nuevo Test{% endif %} | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
|
||||
<h1 class="display-6 fw-bold mb-1">{% if prueba %}Editar Test{% else %}Nuevo Test{% endif %}</h1>
|
||||
<p class="text-white-50 mb-0">{% if prueba %}{{ prueba.nombre }}{% else %}Rellena los datos del nuevo test.{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4" style="overflow-y: auto;">
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.nombre.id_for_label }}" class="form-label fw-semibold">Nombre del test</label>
|
||||
<input type="text" name="nombre" id="{{ form.nombre.id_for_label }}"
|
||||
class="form-control {% if form.nombre.errors %}is-invalid{% endif %}"
|
||||
value="{{ form.nombre.value|default:'' }}" placeholder="Ej: Test de frenada">
|
||||
{% for error in form.nombre.errors %}
|
||||
<div class="invalid-feedback">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.descripcion.id_for_label }}" class="form-label fw-semibold">Objetivo / Descripción</label>
|
||||
<textarea name="descripcion" id="{{ form.descripcion.id_for_label }}" rows="3"
|
||||
class="form-control {% if form.descripcion.errors %}is-invalid{% endif %}">{{ form.descripcion.value|default:'' }}</textarea>
|
||||
{% for error in form.descripcion.errors %}
|
||||
<div class="invalid-feedback">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label for="{{ form.fecha_inicio.id_for_label }}" class="form-label fw-semibold">Fecha de inicio</label>
|
||||
{{ form.fecha_inicio }}
|
||||
{% for error in form.fecha_inicio.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="{{ form.fecha_fin.id_for_label }}" class="form-label fw-semibold">Fecha de fin</label>
|
||||
{{ form.fecha_fin }}
|
||||
{% for error in form.fecha_fin.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label for="{{ form.categoria.id_for_label }}" class="form-label fw-semibold">Categoría</label>
|
||||
<select name="categoria" id="{{ form.categoria.id_for_label }}" class="form-select">
|
||||
{% for valor, etiqueta in form.fields.categoria.choices %}
|
||||
<option value="{{ valor }}" {% if form.categoria.value == valor %}selected{% endif %}>{{ etiqueta }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="{{ form.temporada.id_for_label }}" class="form-label fw-semibold">Temporada</label>
|
||||
<select name="temporada" id="{{ form.temporada.id_for_label }}"
|
||||
class="form-select {% if form.temporada.errors %}is-invalid{% endif %}">
|
||||
{% for temporada in form.fields.temporada.queryset %}
|
||||
<option value="{{ temporada.pk }}" {% if form.temporada.value|stringformat:"s" == temporada.pk|stringformat:"s" %}selected{% endif %}>{{ temporada.nombre }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% for error in form.temporada.errors %}
|
||||
<div class="invalid-feedback">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="{{ form.resultados.id_for_label }}" class="form-label fw-semibold">Conclusiones y Resultados</label>
|
||||
<textarea name="resultados" id="{{ form.resultados.id_for_label }}" rows="3"
|
||||
class="form-control {% if form.resultados.errors %}is-invalid{% endif %}">{{ form.resultados.value|default:'' }}</textarea>
|
||||
{% for error in form.resultados.errors %}
|
||||
<div class="invalid-feedback">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary fw-bold px-4">Guardar</button>
|
||||
<a href="{% if prueba %}{% url 'detalle_prueba' prueba.pk %}{% else %}{% url 'listado_pruebas' %}{% endif %}" class="btn btn-outline-secondary px-4">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% if temporada %}Editar Temporada{% else %}Nueva Temporada{% endif %} | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
|
||||
<h1 class="display-6 fw-bold mb-1">{% if temporada %}Editar Temporada{% else %}Nueva Temporada{% endif %}</h1>
|
||||
<p class="text-white-50 mb-0">{% if temporada %}{{ temporada.nombre }}{% else %}Rellena los datos de la nueva temporada.{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4" style="overflow-y: auto;">
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.nombre.id_for_label }}" class="form-label fw-semibold">Nombre</label>
|
||||
<input type="text" name="nombre" id="{{ form.nombre.id_for_label }}"
|
||||
class="form-control {% if form.nombre.errors %}is-invalid{% endif %}"
|
||||
value="{{ form.nombre.value|default:'' }}" placeholder="Ej: Gades 2025-26">
|
||||
{% for error in form.nombre.errors %}
|
||||
<div class="invalid-feedback">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label for="{{ form.fecha_inicio.id_for_label }}" class="form-label fw-semibold">Fecha de inicio</label>
|
||||
{{ form.fecha_inicio }}
|
||||
{% for error in form.fecha_inicio.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="{{ form.fecha_fin.id_for_label }}" class="form-label fw-semibold">Fecha de fin</label>
|
||||
{{ form.fecha_fin }}
|
||||
{% for error in form.fecha_fin.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.presupuesto.id_for_label }}" class="form-label fw-semibold">Presupuesto (€)</label>
|
||||
<input type="number" name="presupuesto" id="{{ form.presupuesto.id_for_label }}"
|
||||
class="form-control {% if form.presupuesto.errors %}is-invalid{% endif %}"
|
||||
value="{{ form.presupuesto.value|default:'0.00' }}" step="0.01" min="0">
|
||||
{% for error in form.presupuesto.errors %}
|
||||
<div class="invalid-feedback">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mb-4 form-check">
|
||||
<input type="checkbox" name="actual" id="{{ form.actual.id_for_label }}"
|
||||
class="form-check-input" {% if form.actual.value %}checked{% endif %}>
|
||||
<label for="{{ form.actual.id_for_label }}" class="form-check-label fw-semibold">
|
||||
Marcar como temporada activa
|
||||
</label>
|
||||
<div class="form-text">Solo puede haber una temporada activa. Al marcar esta se desactivarán las demás automáticamente.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">Miembros</label>
|
||||
<div class="border rounded p-3" style="max-height: 220px; overflow-y: auto;">
|
||||
{% for checkbox in form.miembros %}
|
||||
<div class="form-check">
|
||||
{{ checkbox.tag }}
|
||||
<label class="form-check-label" for="{{ checkbox.id_for_label }}">
|
||||
{{ checkbox.choice_label }}
|
||||
</label>
|
||||
</div>
|
||||
{% empty %}
|
||||
<span class="text-secondary small">No hay usuarios en el sistema.</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary fw-bold px-4">Guardar</button>
|
||||
<a href="{% url 'gestion_temporadas' %}" class="btn btn-outline-secondary px-4">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Editar Usuario | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
|
||||
<h1 class="display-6 fw-bold mb-1">Editar Usuario</h1>
|
||||
<p class="text-white-50 mb-0">{{ usuario.username }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4" style="overflow-y: auto;">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="card-title fw-bold mb-3">Datos del Usuario</h5>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.username.id_for_label }}" class="form-label fw-semibold">{{ form.username.label }}</label>
|
||||
{{ form.username }}
|
||||
{% for error in form.username.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6 mb-3">
|
||||
<label for="{{ form.first_name.id_for_label }}" class="form-label fw-semibold">{{ form.first_name.label }}</label>
|
||||
{{ form.first_name }}
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label for="{{ form.last_name.id_for_label }}" class="form-label fw-semibold">{{ form.last_name.label }}</label>
|
||||
{{ form.last_name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.email.id_for_label }}" class="form-label fw-semibold">{{ form.email.label }}</label>
|
||||
{{ form.email }}
|
||||
{% for error in form.email.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6 mb-3">
|
||||
<label for="{{ form.rol.id_for_label }}" class="form-label fw-semibold">{{ form.rol.label }}</label>
|
||||
{{ form.rol }}
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label for="{{ form.especialidad.id_for_label }}" class="form-label fw-semibold">{{ form.especialidad.label }}</label>
|
||||
{{ form.especialidad }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary fw-bold px-4">Guardar Cambios</button>
|
||||
<a href="{% url 'gestion_usuarios' %}" class="btn btn-outline-secondary px-4">Cancelar</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Gestión de Temporadas | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm d-flex justify-content-between align-items-center" style="border-left: 5px solid #0d6efd;">
|
||||
<div>
|
||||
<h1 class="display-6 fw-bold mb-1">Gestión de Temporadas</h1>
|
||||
<p class="text-white-50 mb-0">Crea, edita y elimina temporadas del equipo.</p>
|
||||
</div>
|
||||
<a href="{% url 'crear_temporada' %}" class="btn btn-primary fw-bold">+ Nueva Temporada</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4" style="overflow-y: auto;">
|
||||
|
||||
{% if messages %}
|
||||
<div class="col-12">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-success py-2 small mb-0">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Fecha inicio</th>
|
||||
<th>Fecha fin</th>
|
||||
<th>Presupuesto</th>
|
||||
<th>Miembros</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for temporada in temporadas %}
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ temporada.nombre }}</td>
|
||||
<td>{{ temporada.fecha_inicio|date:"d/m/Y" }}</td>
|
||||
<td>{{ temporada.fecha_fin|date:"d/m/Y" }}</td>
|
||||
<td>{{ temporada.presupuesto|floatformat:2 }} €</td>
|
||||
<td>{{ temporada.miembros.count }}</td>
|
||||
<td>
|
||||
{% if temporada.actual %}
|
||||
<span class="badge bg-success">Activa</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Anterior</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="d-flex gap-2">
|
||||
<a href="{% url 'editar_temporada' temporada.pk %}" class="btn btn-sm btn-outline-primary">Editar</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#eliminarModal{{ temporada.pk }}">Eliminar</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<div class="modal fade" id="eliminarModal{{ temporada.pk }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Confirmar eliminación</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
¿Seguro que quieres eliminar la temporada <strong>{{ temporada.nombre }}</strong>? Esta acción no se puede deshacer.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<form method="post" action="{% url 'eliminar_temporada' temporada.pk %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger fw-bold">Eliminar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-secondary py-4">No hay temporadas creadas todavía.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Gestión de Usuarios | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
|
||||
<h1 class="display-6 fw-bold mb-1">Gestión de Usuarios</h1>
|
||||
<p class="text-white-50 mb-0">Administra las cuentas del sistema: edita o elimina usuarios.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4" style="overflow-y: auto;">
|
||||
|
||||
{% if messages %}
|
||||
<div class="col-12">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-success py-2 small mb-0">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
|
||||
<form method="get" class="row g-2 align-items-end mb-4">
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label small text-secondary mb-1">Rol</label>
|
||||
<select name="rol" class="form-select">
|
||||
<option value="">Todos los roles</option>
|
||||
{% for value, label in rol_choices %}
|
||||
<option value="{{ value }}" {% if filtro_rol == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label small text-secondary mb-1">Área Técnica</label>
|
||||
<select name="especialidad" class="form-select">
|
||||
<option value="">Todas las áreas</option>
|
||||
{% for value, label in especialidad_choices %}
|
||||
<option value="{{ value }}" {% if filtro_especialidad == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label small text-secondary mb-1">Apellido</label>
|
||||
<input type="text" name="apellido" class="form-control" placeholder="Buscar por apellido..." value="{{ filtro_apellido }}">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-2 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary fw-bold flex-grow-1">Buscar</button>
|
||||
{% if filtro_rol or filtro_especialidad or filtro_apellido %}
|
||||
<a href="{% url 'gestion_usuarios' %}" class="btn btn-outline-secondary" title="Limpiar filtros">×</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Usuario</th>
|
||||
<th>Nombre</th>
|
||||
<th>Apellidos</th>
|
||||
<th>Email</th>
|
||||
<th>Área Técnica</th>
|
||||
<th>Rol</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for usuario in usuarios %}
|
||||
<tr>
|
||||
<td>{{ usuario.username }}</td>
|
||||
<td>{{ usuario.first_name }}</td>
|
||||
<td>{{ usuario.last_name }}</td>
|
||||
<td>{{ usuario.email }}</td>
|
||||
<td>{{ usuario.get_especialidad_display|default:"—" }}</td>
|
||||
<td>{{ usuario.get_rol_display }}</td>
|
||||
<td class="d-flex gap-2">
|
||||
<a href="{% url 'editar_usuario' usuario.pk %}" class="btn btn-sm btn-outline-primary">Editar</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#eliminarModal{{ usuario.pk }}">Eliminar</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<div class="modal fade" id="eliminarModal{{ usuario.pk }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Confirmar eliminación</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
¿Seguro que quieres eliminar al usuario <strong>{{ usuario.username }}</strong>? Esta acción no se puede deshacer.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<form method="post" action="{% url 'eliminar_usuario' usuario.pk %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger fw-bold">Eliminar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-secondary py-4">No se han encontrado usuarios con los filtros seleccionados.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Inicio | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm d-flex justify-content-between align-items-center flex-wrap" style="border-left: 5px solid #0d6efd;">
|
||||
<div>
|
||||
<h1 class="display-6 fw-bold mb-1">Bienvenido, {{ user.first_name }} {{ user.last_name }}</h1>
|
||||
<p class="text-white-50 mb-0">Panel de control de la plataforma de gestión interna.</p>
|
||||
</div>
|
||||
<div class="text-end mt-2 mt-md-0">
|
||||
<span class="badge bg-primary fs-6 px-3 py-2 shadow-sm">
|
||||
Temporada Activa: {% if temporada_actual %}{{ temporada_actual.nombre }}{% else %}Ninguna activa{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 flex-grow-1" style="min-height: 0;">
|
||||
|
||||
<div class="col-12 col-lg-8 d-flex flex-column" style="min-height: 0;">
|
||||
<div class="shadow rounded-3 overflow-hidden flex-grow-1" style="min-height: 0;">
|
||||
<img src="{% static 'images/monoplaza_gades.jpg' %}" alt="Monoplaza Formula Gades"
|
||||
style="width: 100%; height: 100%; object-fit: cover; display: block;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="row g-3">
|
||||
|
||||
{% if user.rol != 'directiva' %}
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<h5 class="card-title fw-bold text-dark mb-1">Mi Área Técnica</h5>
|
||||
<p class="text-secondary small mb-2">Acceso a documentación, planos y buzón de compras de tu departamento.</p>
|
||||
<a href="#" class="btn btn-outline-primary btn-sm fw-bold px-3">Acceder a mi Área</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<h5 class="card-title fw-bold text-dark mb-1">Pruebas y Telemetría</h5>
|
||||
<p class="text-secondary small mb-2">Carga de archivos de registros CSV y procesamiento analítico de telemetría.</p>
|
||||
<a href="#" class="btn btn-outline-primary btn-sm fw-bold px-3">Ver Telemetría</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<h5 class="card-title fw-bold text-dark mb-1">Miembros del Equipo</h5>
|
||||
<p class="text-secondary small mb-2">Directorio completo de integrantes, correos corporativos y roles.</p>
|
||||
<a href="{% url 'listado_miembros' %}" class="btn btn-outline-primary btn-sm fw-bold px-3">Ver Directorio</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<h5 class="card-title fw-bold text-dark mb-1">Pruebas y Telemetría</h5>
|
||||
<p class="text-secondary small mb-2">Carga de archivos de registros CSV y procesamiento analítico de telemetría.</p>
|
||||
<a href="#" class="btn btn-outline-primary btn-sm fw-bold px-3">Ver Telemetría</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<h5 class="card-title fw-bold text-dark mb-1">Miembros del Equipo</h5>
|
||||
<p class="text-secondary small mb-2">Directorio completo de integrantes, correos corporativos y roles.</p>
|
||||
<a href="{% url 'listado_miembros' %}" class="btn btn-outline-primary btn-sm fw-bold px-3">Ver Directorio</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm" style="border-left: 4px solid #ffc107;">
|
||||
<div class="card-body p-3">
|
||||
<h5 class="card-title fw-bold text-dark mb-1">Contabilidad Global</h5>
|
||||
<p class="text-secondary small mb-2">Supervisión del presupuesto anual y control del buzón financiero de facturas.</p>
|
||||
<a href="#" class="btn btn-warning btn-sm fw-bold text-dark px-3">Gestionar Finanzas</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm" style="border-left: 4px solid #ffc107;">
|
||||
<div class="card-body p-3">
|
||||
<h5 class="card-title fw-bold text-dark mb-1">Administración del Sistema</h5>
|
||||
<p class="text-secondary small mb-2">Módulo avanzado de configuración de permisos, usuarios y temporadas.</p>
|
||||
<a href="#" class="btn btn-warning btn-sm fw-bold text-dark px-3">Panel de Control</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Miembros | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
|
||||
<h1 class="display-6 fw-bold mb-1">Miembros del Equipo</h1>
|
||||
<p class="text-white-50 mb-0">Directorio completo de integrantes de Formula Gades.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4" style="overflow-y: auto;">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
|
||||
<form method="get" class="row g-2 align-items-end mb-4">
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label small text-secondary mb-1">Rol</label>
|
||||
<select name="rol" class="form-select">
|
||||
<option value="">Todos los roles</option>
|
||||
{% for value, label in rol_choices %}
|
||||
<option value="{{ value }}" {% if filtro_rol == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label small text-secondary mb-1">Área Técnica</label>
|
||||
<select name="especialidad" class="form-select">
|
||||
<option value="">Todas las áreas</option>
|
||||
{% for value, label in especialidad_choices %}
|
||||
<option value="{{ value }}" {% if filtro_especialidad == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label small text-secondary mb-1">Apellido</label>
|
||||
<input type="text" name="apellido" class="form-control" placeholder="Buscar por apellido..." value="{{ filtro_apellido }}">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-2 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary fw-bold flex-grow-1">Buscar</button>
|
||||
{% if filtro_rol or filtro_especialidad or filtro_apellido %}
|
||||
<a href="{% url 'listado_miembros' %}" class="btn btn-outline-secondary" title="Limpiar filtros">×</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Usuario</th>
|
||||
<th>Nombre</th>
|
||||
<th>Apellidos</th>
|
||||
<th>Email</th>
|
||||
<th>Área Técnica</th>
|
||||
<th>Rol</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for miembro in miembros %}
|
||||
<tr>
|
||||
<td>{{ miembro.username }}</td>
|
||||
<td>{{ miembro.first_name }}</td>
|
||||
<td>{{ miembro.last_name }}</td>
|
||||
<td>{{ miembro.email }}</td>
|
||||
<td>{{ miembro.get_especialidad_display|default:"—" }}</td>
|
||||
<td>{{ miembro.get_rol_display }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-secondary py-4">No se han encontrado miembros con los filtros seleccionados.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Pruebas | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm d-flex justify-content-between align-items-center" style="border-left: 5px solid #0d6efd;">
|
||||
<div>
|
||||
<h1 class="display-6 fw-bold mb-1">Pruebas</h1>
|
||||
<p class="text-white-50 mb-0">Tests realizados durante la temporada.</p>
|
||||
</div>
|
||||
{% if user.rol == 'directiva' or user.rol == 'jefe_area' %}
|
||||
<a href="{% url 'crear_prueba' %}" class="btn btn-primary fw-bold">+ Nuevo Test</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4" style="overflow-y: auto;">
|
||||
|
||||
{% if messages %}
|
||||
<div class="col-12">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-success py-2 small mb-0">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
|
||||
<form method="get" class="row g-2 align-items-end mb-4">
|
||||
<div class="col-auto">
|
||||
<label class="form-label fw-semibold small mb-1">Temporada</label>
|
||||
<select name="temporada" class="form-select form-select-sm">
|
||||
{% for temporada in temporadas %}
|
||||
<option value="{{ temporada.pk }}" {% if temporada_seleccionada == temporada.pk|stringformat:"s" %}selected{% endif %}>{{ temporada.nombre }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label fw-semibold small mb-1">Categoría</label>
|
||||
<select name="categoria" class="form-select form-select-sm">
|
||||
<option value="">Todas</option>
|
||||
{% for valor, etiqueta in categorias %}
|
||||
<option value="{{ valor }}" {% if categoria_seleccionada == valor %}selected{% endif %}>{{ etiqueta }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">Buscar</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Fecha inicio</th>
|
||||
<th>Fecha fin</th>
|
||||
<th>Categoría</th>
|
||||
<th>Realizado por</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for prueba in pruebas %}
|
||||
<tr role="button" onclick="window.location='{% url 'detalle_prueba' prueba.pk %}'">
|
||||
<td class="fw-semibold">{{ prueba.nombre }}</td>
|
||||
<td>{{ prueba.fecha_inicio|date:"d/m/Y" }}</td>
|
||||
<td>{{ prueba.fecha_fin|date:"d/m/Y" }}</td>
|
||||
<td>{{ prueba.get_categoria_display }}</td>
|
||||
<td>{{ prueba.realizado_por.first_name|default:"—" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-secondary py-4">No hay tests todavía.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Iniciar Sesión | Formula Gades</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
background-color: #111;
|
||||
}
|
||||
.login-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: url("{% static 'images/monoplaza_gades.jpg' %}");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: brightness(0.35);
|
||||
z-index: 0;
|
||||
}
|
||||
.login-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: rgba(20, 20, 20, 0.88);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.form-control {
|
||||
background-color: #1e1e1e;
|
||||
border-color: #444;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
.form-control:focus {
|
||||
background-color: #1e1e1e;
|
||||
border-color: #0d6efd;
|
||||
color: #f0f0f0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(13,110,253,.25);
|
||||
}
|
||||
.form-control::placeholder { color: #888; }
|
||||
.form-label { color: #ccc; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="d-flex align-items-center justify-content-center min-vh-100">
|
||||
|
||||
<div class="login-bg"></div>
|
||||
|
||||
<div class="login-card rounded-4 shadow-lg p-4 p-md-5" style="width: 100%; max-width: 420px;">
|
||||
|
||||
<div class="text-center mb-4">
|
||||
<img src="{% static 'images/logo_gades.png' %}" alt="Formula Gades" height="60" class="mb-3">
|
||||
<h4 class="text-white fw-bold mb-1">Plataforma de gestión interna</h4>
|
||||
<p class="text-secondary small">Bienvenido al sistema interno del equipo.</p>
|
||||
</div>
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger py-2 small" role="alert">
|
||||
Usuario o contraseña incorrectos. Inténtalo de nuevo.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_username" class="form-label fw-semibold">Usuario</label>
|
||||
<input type="text" name="username" id="id_username"
|
||||
class="form-control"
|
||||
placeholder="Introduce tu usuario"
|
||||
autofocus required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="id_password" class="form-label fw-semibold">Contraseña</label>
|
||||
<input type="password" name="password" id="id_password"
|
||||
class="form-control"
|
||||
placeholder="Introduce tu contraseña"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary fw-bold py-2">
|
||||
Iniciar Sesión
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Mi Perfil | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
|
||||
<h1 class="display-6 fw-bold mb-1">Mi Perfil</h1>
|
||||
<p class="text-white-50 mb-0">Consulta y edita tus datos personales.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4" style="overflow-y: auto;">
|
||||
|
||||
{% if messages %}
|
||||
<div class="col-12">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-success py-2 small mb-0">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="card-title fw-bold mb-3">Datos Personales</h5>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label small text-secondary mb-1">Rol en el equipo</label>
|
||||
<input type="text" class="form-control" value="{{ user.get_rol_display }}" disabled>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label small text-secondary mb-1">Área técnica</label>
|
||||
<input type="text" class="form-control" value="{{ user.get_especialidad_display|default:'—' }}" disabled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ perfil_form.username.id_for_label }}" class="form-label fw-semibold">{{ perfil_form.username.label }}</label>
|
||||
{{ perfil_form.username }}
|
||||
{% for error in perfil_form.username.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6 mb-3">
|
||||
<label for="{{ perfil_form.first_name.id_for_label }}" class="form-label fw-semibold">{{ perfil_form.first_name.label }}</label>
|
||||
{{ perfil_form.first_name }}
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label for="{{ perfil_form.last_name.id_for_label }}" class="form-label fw-semibold">{{ perfil_form.last_name.label }}</label>
|
||||
{{ perfil_form.last_name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ perfil_form.email.id_for_label }}" class="form-label fw-semibold">{{ perfil_form.email.label }}</label>
|
||||
{{ perfil_form.email }}
|
||||
{% for error in perfil_form.email.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button type="submit" name="guardar_perfil" class="btn btn-primary fw-bold px-4">Guardar Cambios</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="card-title fw-bold mb-3">Cambiar Contraseña</h5>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ password_form.old_password.id_for_label }}" class="form-label fw-semibold">Contraseña actual</label>
|
||||
<input type="password" name="{{ password_form.old_password.html_name }}" id="{{ password_form.old_password.id_for_label }}" class="form-control">
|
||||
{% for error in password_form.old_password.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ password_form.new_password1.id_for_label }}" class="form-label fw-semibold">Nueva contraseña</label>
|
||||
<input type="password" name="{{ password_form.new_password1.html_name }}" id="{{ password_form.new_password1.id_for_label }}" class="form-control">
|
||||
{% for error in password_form.new_password1.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ password_form.new_password2.id_for_label }}" class="form-label fw-semibold">Confirmar nueva contraseña</label>
|
||||
<input type="password" name="{{ password_form.new_password2.html_name }}" id="{{ password_form.new_password2.id_for_label }}" class="form-control">
|
||||
{% for error in password_form.new_password2.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if password_form.non_field_errors %}
|
||||
<div class="text-danger small mb-3">{{ password_form.non_field_errors }}</div>
|
||||
{% endif %}
|
||||
|
||||
<button type="submit" name="cambiar_password" class="btn btn-outline-danger fw-bold px-4">Cambiar Contraseña</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Patrocinios | Gades Manager{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
<div class="bg-dark text-white p-3 rounded-3 shadow-sm d-flex justify-content-between align-items-center" style="border-left: 5px solid #0d6efd;">
|
||||
<div>
|
||||
<h1 class="display-6 fw-bold mb-1">Patrocinios</h1>
|
||||
<p class="text-white-50 mb-0">Dossiers y gestión de patrocinadores del equipo.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary fw-bold" data-bs-toggle="modal" data-bs-target="#modalProponer">+ Proponer Patrocinio</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if messages %}
|
||||
<div class="row mb-2 flex-shrink-0">
|
||||
<div class="col-12">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %} py-2 small mb-1">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-4" style="overflow-y: auto;">
|
||||
|
||||
{# ── Bloque 1: Dossiers PDF ── #}
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4 d-flex flex-column align-items-center text-center">
|
||||
<div class="mb-3" style="font-size: 3rem; color: #dc3545;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
|
||||
<path d="M4.603 14.087a.81.81 0 0 1-.438-.42c-.195-.388-.13-.776.08-1.102.198-.307.526-.568.897-.787a7.68 7.68 0 0 1 1.482-.645 19.697 19.697 0 0 0 1.062-2.227 7.269 7.269 0 0 1-.43-1.295c-.086-.4-.119-.796-.046-1.136.075-.354.274-.672.65-.823.192-.077.4-.12.602-.077a.7.7 0 0 1 .477.365c.088.164.12.356.127.538.007.188-.012.396-.047.614-.084.51-.27 1.134-.52 1.794a10.954 10.954 0 0 0 .98 1.686 5.753 5.753 0 0 1 1.334.05c.364.066.734.195.96.465.12.144.193.32.2.518.007.192-.047.382-.138.563a1.04 1.04 0 0 1-.354.416.856.856 0 0 1-.51.138c-.331-.014-.654-.196-.933-.417a5.712 5.712 0 0 1-.911-.95 11.651 11.651 0 0 0-1.997.406 11.307 11.307 0 0 1-1.02 1.51c-.292.35-.609.656-.927.787a.793.793 0 0 1-.58.029z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h5 class="fw-bold mb-1">Dossier de Patrocinio</h5>
|
||||
<p class="text-secondary small mb-3">Versión en español · Temporada 2025/2026</p>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% static 'dossiers/Dossier_2025_2026.pdf' %}" target="_blank" class="btn btn-outline-primary fw-bold">Abrir</a>
|
||||
<a href="{% static 'dossiers/Dossier_2025_2026.pdf' %}" download class="btn btn-primary fw-bold">Descargar</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4 d-flex flex-column align-items-center text-center">
|
||||
<div class="mb-3" style="font-size: 3rem; color: #dc3545;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
|
||||
<path d="M4.603 14.087a.81.81 0 0 1-.438-.42c-.195-.388-.13-.776.08-1.102.198-.307.526-.568.897-.787a7.68 7.68 0 0 1 1.482-.645 19.697 19.697 0 0 0 1.062-2.227 7.269 7.269 0 0 1-.43-1.295c-.086-.4-.119-.796-.046-1.136.075-.354.274-.672.65-.823.192-.077.4-.12.602-.077a.7.7 0 0 1 .477.365c.088.164.12.356.127.538.007.188-.012.396-.047.614-.084.51-.27 1.134-.52 1.794a10.954 10.954 0 0 0 .98 1.686 5.753 5.753 0 0 1 1.334.05c.364.066.734.195.96.465.12.144.193.32.2.518.007.192-.047.382-.138.563a1.04 1.04 0 0 1-.354.416.856.856 0 0 1-.51.138c-.331-.014-.654-.196-.933-.417a5.712 5.712 0 0 1-.911-.95 11.651 11.651 0 0 0-1.997.406 11.307 11.307 0 0 1-1.02 1.51c-.292.35-.609.656-.927.787a.793.793 0 0 1-.58.029z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h5 class="fw-bold mb-1">Sponsorship Dossier</h5>
|
||||
<p class="text-secondary small mb-3">English version · Season 2025/2026</p>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% static 'dossiers/DossierEN_2025_2026.pdf' %}" target="_blank" class="btn btn-outline-primary fw-bold">Open</a>
|
||||
<a href="{% static 'dossiers/DossierEN_2025_2026.pdf' %}" download class="btn btn-primary fw-bold">Download</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Bloque 2: Patrocinios agrupados ── #}
|
||||
<div class="col-12">
|
||||
|
||||
{% if not temporada_actual %}
|
||||
<div class="alert alert-warning">No hay temporada activa. Activa una desde <a href="{% url 'gestion_temporadas' %}">Gestión de Temporadas</a>.</div>
|
||||
{% else %}
|
||||
|
||||
{# Pendientes #}
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-white border-bottom d-flex align-items-center gap-2 py-3">
|
||||
<span class="badge bg-warning text-dark fs-6">{{ pendientes.count }}</span>
|
||||
<h6 class="fw-bold mb-0">Pendientes / En contacto</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Empresa</th>
|
||||
<th>Tipo</th>
|
||||
<th>Persona contacto</th>
|
||||
<th>Email</th>
|
||||
<th>Propuesto por</th>
|
||||
<th>Fecha</th>
|
||||
{% if user.rol == 'directiva' %}<th>Acciones</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in pendientes %}
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ p.empresa }}</td>
|
||||
<td>{{ p.get_tipo_patrocinio_display }}</td>
|
||||
<td>{{ p.persona_contacto|default:"—" }}</td>
|
||||
<td>{{ p.email_contacto }}</td>
|
||||
<td>{{ p.contacto_equipo.get_full_name|default:p.contacto_equipo }}</td>
|
||||
<td>{{ p.fecha_contacto|date:"d/m/Y" }}</td>
|
||||
{% if user.rol == 'directiva' %}
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<form method="post" action="{% url 'cambiar_estado_patrocinio' p.pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="estado" value="aceptado">
|
||||
<button type="submit" class="btn btn-sm btn-success fw-bold">Aceptar</button>
|
||||
</form>
|
||||
<form method="post" action="{% url 'cambiar_estado_patrocinio' p.pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="estado" value="denegado">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">Rechazar</button>
|
||||
</form>
|
||||
<a href="{% url 'editar_patrocinio' p.pk %}" class="btn btn-sm btn-outline-secondary">Editar</a>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="{% if user.rol == 'directiva' %}7{% else %}6{% endif %}" class="text-center text-secondary py-3">No hay patrocinios pendientes.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Aceptados #}
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-white border-bottom d-flex align-items-center gap-2 py-3">
|
||||
<span class="badge bg-success fs-6">{{ aceptados.count }}</span>
|
||||
<h6 class="fw-bold mb-0">Aceptados</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Empresa</th>
|
||||
<th>Tipo</th>
|
||||
<th>Importe</th>
|
||||
<th>Persona contacto</th>
|
||||
<th>Email</th>
|
||||
<th>Fecha</th>
|
||||
{% if user.rol == 'directiva' %}<th>Acciones</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in aceptados %}
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ p.empresa }}</td>
|
||||
<td>{{ p.get_tipo_patrocinio_display }}</td>
|
||||
<td>{{ p.importe_economico|floatformat:2 }} €</td>
|
||||
<td>{{ p.persona_contacto|default:"—" }}</td>
|
||||
<td>{{ p.email_contacto }}</td>
|
||||
<td>{{ p.fecha_contacto|date:"d/m/Y" }}</td>
|
||||
{% if user.rol == 'directiva' %}
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<form method="post" action="{% url 'cambiar_estado_patrocinio' p.pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="estado" value="en_contacto">
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning">Revertir</button>
|
||||
</form>
|
||||
<a href="{% url 'editar_patrocinio' p.pk %}" class="btn btn-sm btn-outline-secondary">Editar</a>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="{% if user.rol == 'directiva' %}7{% else %}6{% endif %}" class="text-center text-secondary py-3">No hay patrocinios aceptados.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Rechazados — colapsado por defecto #}
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-bottom d-flex align-items-center gap-2 py-3" style="cursor:pointer;" data-bs-toggle="collapse" data-bs-target="#collapseRechazados">
|
||||
<span class="badge bg-danger fs-6">{{ denegados.count }}</span>
|
||||
<h6 class="fw-bold mb-0 text-secondary">Rechazados</h6>
|
||||
<span class="ms-auto text-secondary small">▼</span>
|
||||
</div>
|
||||
<div class="collapse" id="collapseRechazados">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Empresa</th>
|
||||
<th>Tipo</th>
|
||||
<th>Persona contacto</th>
|
||||
<th>Email</th>
|
||||
<th>Fecha</th>
|
||||
{% if user.rol == 'directiva' %}<th>Acciones</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in denegados %}
|
||||
<tr class="text-secondary">
|
||||
<td class="fw-semibold">{{ p.empresa }}</td>
|
||||
<td>{{ p.get_tipo_patrocinio_display }}</td>
|
||||
<td>{{ p.persona_contacto|default:"—" }}</td>
|
||||
<td>{{ p.email_contacto }}</td>
|
||||
<td>{{ p.fecha_contacto|date:"d/m/Y" }}</td>
|
||||
{% if user.rol == 'directiva' %}
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<form method="post" action="{% url 'cambiar_estado_patrocinio' p.pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="estado" value="en_contacto">
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning">Reabrir</button>
|
||||
</form>
|
||||
<a href="{% url 'editar_patrocinio' p.pk %}" class="btn btn-sm btn-outline-secondary">Editar</a>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="{% if user.rol == 'directiva' %}6{% else %}5{% endif %}" class="text-center text-secondary py-3">No hay patrocinios rechazados.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}{# end if temporada_actual #}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Modal Proponer Patrocinio ── #}
|
||||
<div class="modal fade" id="modalProponer" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Proponer Patrocinio</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="post" action="{% url 'proponer_patrocinio' %}">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small mt-1">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<p class="text-secondary small mb-0">El estado quedará como <strong>Pendiente</strong> hasta que la directiva lo revise. Tu nombre quedará registrado como proponente.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary fw-bold">Proponer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from .models import Temporada
|
||||
|
||||
@admin.register(Temporada)
|
||||
class TemporadaAdmin(admin.ModelAdmin):
|
||||
# Columnas principales.
|
||||
list_display = ('nombre', 'fecha_inicio', 'fecha_fin', 'presupuesto', 'actual')
|
||||
|
||||
# Permite marcar o desmarcar el check de "actual" directamente desde la tabla, sin entrar al detalle
|
||||
list_editable = ('actual',)
|
||||
|
||||
# Filtro para filtrar por temporadas históricas o actual
|
||||
list_filter = ('actual',)
|
||||
|
||||
# Buscador de temporadas por nombre
|
||||
search_fields = ('nombre',)
|
||||
|
||||
# Filtro para ver los miembros de una temporada
|
||||
filter_horizontal = ('miembros',)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TemporadasConfig(AppConfig):
|
||||
name = 'temporadas'
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
from django import forms
|
||||
from .models import Temporada
|
||||
|
||||
|
||||
class TemporadaForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Temporada
|
||||
fields = ['nombre', 'fecha_inicio', 'fecha_fin', 'presupuesto', 'actual', 'miembros']
|
||||
widgets = {
|
||||
'fecha_inicio': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
'fecha_fin': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||||
'miembros': forms.CheckboxSelectMultiple(),
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-02-17 11:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Temporada',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nombre', models.CharField(max_length=50, unique=True)),
|
||||
('fecha_inicio', models.DateField()),
|
||||
('fecha_fin', models.DateField()),
|
||||
('presupuesto', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)),
|
||||
('actual', models.BooleanField(default=False, verbose_name='¿Es la temporada actual?')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Temporada',
|
||||
'verbose_name_plural': 'Temporadas',
|
||||
'ordering': ['-fecha_inicio'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-03-06 19:29
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('temporadas', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='temporada',
|
||||
name='miembros',
|
||||
field=models.ManyToManyField(blank=True, related_name='temporadas_participadas', to=settings.AUTH_USER_MODEL, verbose_name='Miembros del equipo'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
class Temporada(models.Model):
|
||||
# El nombre será algo como "Gades 2024-25"
|
||||
nombre = models.CharField(max_length=50, unique=True)
|
||||
|
||||
fecha_inicio = models.DateField()
|
||||
fecha_fin = models.DateField()
|
||||
|
||||
# Presupuesto total para ese año
|
||||
presupuesto = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
|
||||
|
||||
# Checkbox para marcar cuál es la temporada que estamos viviendo ahora
|
||||
actual = models.BooleanField(default=False, verbose_name="¿Es la temporada actual?")
|
||||
|
||||
# Relaciones
|
||||
# Relacion N a N con miembros
|
||||
miembros = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
related_name="temporadas_participadas",
|
||||
blank=True,
|
||||
verbose_name="Miembros del equipo"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Temporada"
|
||||
verbose_name_plural = "Temporadas"
|
||||
ordering = ['-fecha_inicio'] # Ordena las más nuevas primero
|
||||
|
||||
def __str__(self):
|
||||
return self.nombre
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# TRUCO PRO: Si marco esta temporada como "actual", desmarco todas las demás
|
||||
# Así evitamos que haya dos temporadas activas a la vez.
|
||||
if self.actual:
|
||||
Temporada.objects.filter(actual=True).exclude(pk=self.pk).update(actual=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.views.decorators.http import require_POST
|
||||
from users.decorators import require_rol
|
||||
from .models import Temporada
|
||||
from .forms import TemporadaForm
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
def gestion_temporadas(request):
|
||||
temporadas = Temporada.objects.all()
|
||||
return render(request, 'gestion_temporadas.html', {'temporadas': temporadas})
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
def crear_temporada(request):
|
||||
form = TemporadaForm(request.POST or None)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Temporada creada correctamente.')
|
||||
return redirect('gestion_temporadas')
|
||||
return render(request, 'editar_temporada.html', {'form': form, 'temporada': None})
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
def editar_temporada(request, pk):
|
||||
temporada = get_object_or_404(Temporada, pk=pk)
|
||||
form = TemporadaForm(request.POST or None, instance=temporada)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Temporada actualizada correctamente.')
|
||||
return redirect('gestion_temporadas')
|
||||
return render(request, 'editar_temporada.html', {'form': form, 'temporada': temporada})
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
@require_POST
|
||||
def eliminar_temporada(request, pk):
|
||||
temporada = get_object_or_404(Temporada, pk=pk)
|
||||
temporada.delete()
|
||||
messages.success(request, f'Temporada "{temporada.nombre}" eliminada.')
|
||||
return redirect('gestion_temporadas')
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from .models import CustomUser
|
||||
|
||||
@admin.register(CustomUser)
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
list_display = ('username', 'first_name', 'last_name', 'rol', 'especialidad', 'is_staff')
|
||||
|
||||
# Filtramos por rol y área técnica
|
||||
list_filter = ('rol', 'especialidad', 'is_staff', 'is_active')
|
||||
|
||||
# Buscador de usuarios
|
||||
search_fields = ('username', 'first_name', 'last_name', 'email')
|
||||
|
||||
# Para editar al usuario con los datos insertados
|
||||
fieldsets = UserAdmin.fieldsets + (
|
||||
('Información del Equipo Gades', {
|
||||
'fields': ('rol', 'especialidad'),
|
||||
}),
|
||||
)
|
||||
|
||||
# Para crear al usuario con los datos insertados
|
||||
add_fieldsets = UserAdmin.add_fieldsets + (
|
||||
('Información del Equipo Gades', {
|
||||
'fields': ('rol', 'especialidad'),
|
||||
}),
|
||||
)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
name = 'users'
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
|
||||
|
||||
def require_rol(*roles):
|
||||
def decorator(view_func):
|
||||
return login_required(user_passes_test(lambda u: u.rol in roles)(view_func))
|
||||
return decorator
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
from django import forms
|
||||
from .models import CustomUser
|
||||
|
||||
|
||||
class PerfilForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ['username', 'first_name', 'last_name', 'email']
|
||||
widgets = {
|
||||
'username': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
labels = {
|
||||
'username': 'Usuario',
|
||||
'first_name': 'Nombre',
|
||||
'last_name': 'Apellidos',
|
||||
'email': 'Correo electrónico',
|
||||
}
|
||||
|
||||
|
||||
class EditarUsuarioForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ['username', 'first_name', 'last_name', 'email', 'rol', 'especialidad']
|
||||
widgets = {
|
||||
'username': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||
'rol': forms.Select(attrs={'class': 'form-select'}),
|
||||
'especialidad': forms.Select(attrs={'class': 'form-select'}),
|
||||
}
|
||||
labels = {
|
||||
'username': 'Usuario',
|
||||
'first_name': 'Nombre',
|
||||
'last_name': 'Apellidos',
|
||||
'email': 'Correo electrónico',
|
||||
'rol': 'Rol en el equipo',
|
||||
'especialidad': 'Área técnica',
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-02-17 10:36
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('rol', models.CharField(choices=[('directiva', 'Directiva'), ('jefe_area', 'Jefe de Área'), ('empleado', 'Empleado')], default='empleado', max_length=20)),
|
||||
('especialidad', models.CharField(blank=True, choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software')], max_length=30, null=True)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-02-17 11:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customuser',
|
||||
name='especialidad',
|
||||
field=models.CharField(blank=True, choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software')], max_length=30, null=True, verbose_name='Área Técnica'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customuser',
|
||||
name='rol',
|
||||
field=models.CharField(choices=[('directiva', 'Directiva'), ('jefe_area', 'Jefe de Área'), ('empleado', 'Empleado')], default='empleado', max_length=20, verbose_name='Rol en el equipo'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 6.0.2 on 2026-03-06 19:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0002_alter_customuser_especialidad_alter_customuser_rol'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customuser',
|
||||
name='especialidad',
|
||||
field=models.CharField(blank=True, choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business_operations', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software')], max_length=30, null=True, verbose_name='Área Técnica'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customuser',
|
||||
name='rol',
|
||||
field=models.CharField(choices=[('directiva', 'Directiva'), ('jefe_area', 'Jefe de Área'), ('miembro', 'Miembro')], default='miembro', max_length=20, verbose_name='Rol en el equipo'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
class CustomUser(AbstractUser):
|
||||
# --- 1. OPCIONES (El Menú) ---
|
||||
ROL_CHOICES = (
|
||||
('directiva', 'Directiva'),
|
||||
('jefe_area', 'Jefe de Área'),
|
||||
('miembro', 'Miembro'),
|
||||
)
|
||||
|
||||
ESPECIALIDAD_CHOICES = (
|
||||
('aerodinamica', 'Aerodinámica'),
|
||||
('chasis', 'Chasis'),
|
||||
('business_operations', 'Business & Operations'),
|
||||
('epowertrain', 'E-Powertrain'),
|
||||
('electronica', 'Electrónica'),
|
||||
('sdf', 'SDF'),
|
||||
('motor_transmision', 'Motor & Transmisión'),
|
||||
('software', 'Software'),
|
||||
)
|
||||
|
||||
# --- 2. CAMPOS (Las Columnas en la BD) ---
|
||||
# Usamos la versión con 'verbose_name' porque queda mejor en la web
|
||||
rol = models.CharField(
|
||||
max_length=20,
|
||||
choices=ROL_CHOICES,
|
||||
default='miembro',
|
||||
verbose_name="Rol en el equipo"
|
||||
)
|
||||
|
||||
especialidad = models.CharField(
|
||||
max_length=30,
|
||||
choices=ESPECIALIDAD_CHOICES,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Área Técnica"
|
||||
)
|
||||
|
||||
# --- 3. MÉTODOS ---
|
||||
def __str__(self):
|
||||
# Muestra: "Nombre Apellido (usuario) - Rol"
|
||||
return f"{self.first_name} {self.last_name} ({self.username}) - {self.get_rol_display()}"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.forms import PasswordChangeForm
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib import messages
|
||||
from django.views.decorators.http import require_POST
|
||||
from .decorators import require_rol
|
||||
from .forms import PerfilForm, EditarUsuarioForm
|
||||
from .models import CustomUser
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
return render(request, 'index.html')
|
||||
|
||||
|
||||
@login_required
|
||||
def mi_perfil(request):
|
||||
perfil_form = PerfilForm(instance=request.user)
|
||||
password_form = PasswordChangeForm(user=request.user)
|
||||
|
||||
if request.method == 'POST':
|
||||
if 'guardar_perfil' in request.POST:
|
||||
perfil_form = PerfilForm(request.POST, instance=request.user)
|
||||
if perfil_form.is_valid():
|
||||
perfil_form.save()
|
||||
messages.success(request, 'Tus datos se han actualizado correctamente.')
|
||||
return redirect('mi_perfil')
|
||||
|
||||
elif 'cambiar_password' in request.POST:
|
||||
password_form = PasswordChangeForm(user=request.user, data=request.POST)
|
||||
if password_form.is_valid():
|
||||
user = password_form.save()
|
||||
update_session_auth_hash(request, user)
|
||||
messages.success(request, 'Tu contraseña se ha cambiado correctamente.')
|
||||
return redirect('mi_perfil')
|
||||
|
||||
context = {
|
||||
'perfil_form': perfil_form,
|
||||
'password_form': password_form,
|
||||
}
|
||||
return render(request, 'mi_perfil.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def listado_miembros(request):
|
||||
miembros = CustomUser.objects.all().order_by('last_name', 'first_name')
|
||||
|
||||
rol = request.GET.get('rol', '')
|
||||
especialidad = request.GET.get('especialidad', '')
|
||||
apellido = request.GET.get('apellido', '')
|
||||
|
||||
if rol:
|
||||
miembros = miembros.filter(rol=rol)
|
||||
if especialidad:
|
||||
miembros = miembros.filter(especialidad=especialidad)
|
||||
if apellido:
|
||||
miembros = miembros.filter(last_name__icontains=apellido)
|
||||
|
||||
context = {
|
||||
'miembros': miembros,
|
||||
'rol_choices': CustomUser.ROL_CHOICES,
|
||||
'especialidad_choices': CustomUser.ESPECIALIDAD_CHOICES,
|
||||
'filtro_rol': rol,
|
||||
'filtro_especialidad': especialidad,
|
||||
'filtro_apellido': apellido,
|
||||
}
|
||||
return render(request, 'listado_miembros.html', context)
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
def gestion_usuarios(request):
|
||||
usuarios = CustomUser.objects.all().order_by('last_name', 'first_name')
|
||||
|
||||
rol = request.GET.get('rol', '')
|
||||
especialidad = request.GET.get('especialidad', '')
|
||||
apellido = request.GET.get('apellido', '')
|
||||
|
||||
if rol:
|
||||
usuarios = usuarios.filter(rol=rol)
|
||||
if especialidad:
|
||||
usuarios = usuarios.filter(especialidad=especialidad)
|
||||
if apellido:
|
||||
usuarios = usuarios.filter(last_name__icontains=apellido)
|
||||
|
||||
context = {
|
||||
'usuarios': usuarios,
|
||||
'rol_choices': CustomUser.ROL_CHOICES,
|
||||
'especialidad_choices': CustomUser.ESPECIALIDAD_CHOICES,
|
||||
'filtro_rol': rol,
|
||||
'filtro_especialidad': especialidad,
|
||||
'filtro_apellido': apellido,
|
||||
}
|
||||
return render(request, 'gestion_usuarios.html', context)
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
def editar_usuario(request, pk):
|
||||
usuario = get_object_or_404(CustomUser, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = EditarUsuarioForm(request.POST, instance=usuario)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'El usuario se ha actualizado correctamente.')
|
||||
return redirect('gestion_usuarios')
|
||||
else:
|
||||
form = EditarUsuarioForm(instance=usuario)
|
||||
|
||||
return render(request, 'editar_usuario.html', {'form': form, 'usuario': usuario})
|
||||
|
||||
|
||||
@require_rol('directiva')
|
||||
@require_POST
|
||||
def eliminar_usuario(request, pk):
|
||||
usuario = get_object_or_404(CustomUser, pk=pk)
|
||||
usuario.delete()
|
||||
messages.success(request, 'El usuario se ha eliminado correctamente.')
|
||||
return redirect('gestion_usuarios')
|
||||
Loading…
Add table
Add a link
Reference in a new issue