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