sección patrocinios añadida

This commit is contained in:
adrigongv23 2026-07-01 12:23:57 +02:00
parent a15f836e07
commit 1d1c7a61e9
8 changed files with 411 additions and 4 deletions

View file

@ -41,4 +41,8 @@ urlpatterns = [
path('gestion/contabilidad/ingreso/anadir/', views.anadir_ingreso, name='anadir_ingreso'), 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>/aceptar/', views.aceptar_factura, name='aceptar_factura'),
path('gestion/contabilidad/factura/<int:pk>/rechazar/', views.rechazar_factura, name='rechazar_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'),
] ]

View file

@ -1,5 +1,5 @@
from django import forms from django import forms
from .models import Gasto, Ingreso from .models import Gasto, Ingreso, Patrocinio
class GastoForm(forms.ModelForm): class GastoForm(forms.ModelForm):
@ -22,3 +22,25 @@ class IngresoForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for field in self.fields.values(): for field in self.fields.values():
field.widget.attrs['class'] = 'form-control' 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'

View file

@ -8,8 +8,8 @@ from datetime import date
from temporadas.models import Temporada from temporadas.models import Temporada
from documentos.models import Factura from documentos.models import Factura
from users.decorators import require_rol from users.decorators import require_rol
from .models import Gasto, Ingreso from .models import Gasto, Ingreso, Patrocinio
from .forms import GastoForm, IngresoForm from .forms import GastoForm, IngresoForm, PatrocinioForm, PatrocinioEditForm
# Categorías válidas en Gasto (Documento usa 'normativa' en su lugar de 'general') # 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} _CATEGORIAS_GASTO_VALIDAS = {c[0] for c in Gasto.CATEGORIAS_GASTOS}
@ -132,3 +132,71 @@ def rechazar_factura(request, pk):
factura.save() factura.save()
messages.success(request, f'Factura de {factura.empresa} rechazada.') messages.success(request, f'Factura de {factura.empresa} rechazada.')
return redirect('contabilidad') 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')

Binary file not shown.

Binary file not shown.

View file

@ -50,7 +50,7 @@
<a class="nav-link" href="#">Pruebas</a> <a class="nav-link" href="#">Pruebas</a>
</li> </li>
<li class="nav-item px-2"> <li class="nav-item px-2">
<a class="nav-link" href="#">Patrocinios</a> <a class="nav-link" href="{% url 'patrocinios' %}">Patrocinios</a>
</li> </li>
<li class="nav-item px-2"> <li class="nav-item px-2">
<a class="nav-link" href="{% url 'listado_miembros' %}">Miembros</a> <a class="nav-link" href="{% url 'listado_miembros' %}">Miembros</a>

View file

@ -0,0 +1,44 @@
{% 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 %}

View file

@ -0,0 +1,269 @@
{% 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 %}