Sección de contabilida añadida

This commit is contained in:
adrigongv23 2026-06-30 19:55:37 +02:00
parent 408d4d006e
commit a15f836e07
7 changed files with 430 additions and 9 deletions

View file

@ -0,0 +1,18 @@
# 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),
),
]

View file

@ -62,11 +62,18 @@ class Documento(models.Model):
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"

View file

@ -36,4 +36,9 @@ urlpatterns = [
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'),
]

View file

@ -0,0 +1,24 @@
from django import forms
from .models import Gasto, Ingreso
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'

View file

@ -1,14 +1,134 @@
from django.shortcuts import render
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
from .forms import GastoForm, IngresoForm
# 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):
# Buscamos la temporada que configuraste como actual
temporada_activa = Temporada.objects.filter(actual=True).first()
context = {
'temporada_actual': temporada_activa
}
# Renderiza index.html, el cual hereda automáticamente de base.html
return render(request, 'index.html', context)
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')

View file

@ -58,7 +58,7 @@
{% if user.is_authenticated and user.rol == 'directiva' %}
<li class="nav-item px-2">
<a class="nav-link text-warning" href="#">Contabilidad</a>
<a class="nav-link text-warning" href="{% url 'contabilidad' %}">Contabilidad</a>
</li>
<li class="nav-item dropdown px-2">

View file

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