mirror of
https://github.com/adrigongv23/G26---Telemetry-Software.git
synced 2026-08-25 11:33:17 +02:00
Sección de contabilida añadida
This commit is contained in:
parent
408d4d006e
commit
a15f836e07
7 changed files with 430 additions and 9 deletions
18
Plataform_Web/documentos/migrations/0003_factura_estado.py
Normal file
18
Plataform_Web/documentos/migrations/0003_factura_estado.py
Normal 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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]
|
||||
24
Plataform_Web/gestion/forms.py
Normal file
24
Plataform_Web/gestion/forms.py
Normal 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'
|
||||
|
|
@ -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()
|
||||
return render(request, 'index.html', {'temporada_actual': temporada_activa})
|
||||
|
||||
context = {
|
||||
'temporada_actual': temporada_activa
|
||||
}
|
||||
# Renderiza index.html, el cual hereda automáticamente de base.html
|
||||
return render(request, 'index.html', context)
|
||||
|
||||
@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')
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
247
Plataform_Web/templates/contabilidad.html
Normal file
247
Plataform_Web/templates/contabilidad.html
Normal 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 %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue