parent
0d9f0905fb
commit
57ff4e304a
@ -0,0 +1 @@ |
||||
venv/ |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,16 @@ |
||||
""" |
||||
ASGI config for RegistroDePagos 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/5.1/howto/deployment/asgi/ |
||||
""" |
||||
|
||||
import os |
||||
|
||||
from django.core.asgi import get_asgi_application |
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'RegistroDePagos.settings') |
||||
|
||||
application = get_asgi_application() |
||||
@ -0,0 +1,39 @@ |
||||
""" |
||||
URL configuration for RegistroDePagos project. |
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see: |
||||
https://docs.djangoproject.com/en/5.1/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 tasks import views |
||||
from django.contrib.auth import views as auth_views |
||||
|
||||
|
||||
urlpatterns = [ |
||||
path('admin/', admin.site.urls), |
||||
path('',views.signin, name='signin'), |
||||
path('signup/',views.signup, name='signup'), |
||||
path('signout/',views.signout, name='signout'), |
||||
path('compras/', views.lista_compras, name='lista_compras'), |
||||
path('compras/crear/', views.crear_compra, name='crear_compra'), |
||||
path('compras/<int:compra_id>/pago/', views.registrar_pago, name='registrar_pago'), |
||||
path('compras/<int:compra_id>/', views.detalle_compra, name='detalle_compra'), |
||||
path('compras/<int:compra_id>/pdf/', views.reporte_detalle_compra, name='reporte_detalle_compra'), |
||||
path('compras/<int:compra_id>/excel/', views.exportar_excel_detalle_compra, name='exportar_excel'), |
||||
path('compras/<int:compra_id>/csv/', views.exportar_csv_detalle_compra, name='exportar_csv'), |
||||
path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'), |
||||
path('password_reset_done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), |
||||
path('password_reset_confirm/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), |
||||
path('password_reset_complete/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), |
||||
] |
||||
@ -0,0 +1,16 @@ |
||||
""" |
||||
WSGI config for RegistroDePagos 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/5.1/howto/deployment/wsgi/ |
||||
""" |
||||
|
||||
import os |
||||
|
||||
from django.core.wsgi import get_wsgi_application |
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'RegistroDePagos.settings') |
||||
|
||||
application = get_wsgi_application() |
||||
@ -0,0 +1,22 @@ |
||||
#!/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', 'RegistroDePagos.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() |
||||
@ -0,0 +1,49 @@ |
||||
arabic-reshaper==3.0.0 |
||||
asgiref==3.8.1 |
||||
asn1crypto==1.5.1 |
||||
Brotli==1.1.0 |
||||
certifi==2024.12.14 |
||||
cffi==1.17.1 |
||||
chardet==5.2.0 |
||||
charset-normalizer==3.4.0 |
||||
click==8.1.7 |
||||
colorama==0.4.6 |
||||
cryptography==44.0.0 |
||||
cssselect2==0.7.0 |
||||
Django==5.1.4 |
||||
et_xmlfile==2.0.0 |
||||
fonttools==4.55.3 |
||||
html5lib==1.1 |
||||
idna==3.10 |
||||
lxml==5.3.0 |
||||
mysqlclient==2.2.6 |
||||
numpy==2.2.0 |
||||
openpyxl==3.1.5 |
||||
oscrypto==1.3.0 |
||||
pandas==2.2.3 |
||||
pillow==11.0.0 |
||||
pycparser==2.22 |
||||
pydyf==0.11.0 |
||||
pyHanko==0.25.3 |
||||
pyhanko-certvalidator==0.26.5 |
||||
pypdf==5.1.0 |
||||
pyphen==0.17.0 |
||||
python-bidi==0.6.3 |
||||
python-dateutil==2.9.0.post0 |
||||
pytz==2024.2 |
||||
PyYAML==6.0.2 |
||||
qrcode==8.0 |
||||
reportlab==4.2.5 |
||||
requests==2.32.3 |
||||
six==1.17.0 |
||||
sqlparse==0.5.3 |
||||
svglib==1.5.1 |
||||
tinycss2==1.4.0 |
||||
tinyhtml5==2.0.0 |
||||
tzdata==2024.2 |
||||
tzlocal==5.2 |
||||
uritools==4.0.3 |
||||
urllib3==2.2.3 |
||||
webencodings==0.5.1 |
||||
xhtml2pdf==0.2.16 |
||||
zopfli==0.2.3.post1 |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,3 @@ |
||||
from django.contrib import admin |
||||
|
||||
# Register your models here. |
||||
@ -0,0 +1,6 @@ |
||||
from django.apps import AppConfig |
||||
|
||||
|
||||
class TasksConfig(AppConfig): |
||||
default_auto_field = 'django.db.models.BigAutoField' |
||||
name = 'tasks' |
||||
@ -0,0 +1,22 @@ |
||||
from django import forms |
||||
from .models import RegistrarCompra, RegistroPagos |
||||
|
||||
class RegistrarCompraForm(forms.ModelForm): |
||||
class Meta: |
||||
model = RegistrarCompra |
||||
fields = ['nombre_compra', 'monto_pago', 'observacion'] |
||||
widgets = { |
||||
'nombre_compra': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Nombre de la compra'}), |
||||
'monto_pago': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'Monto a pagar'}), |
||||
'observacion': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Observaciones'}), |
||||
} |
||||
|
||||
class RegistroPagosForm(forms.ModelForm): |
||||
class Meta: |
||||
model = RegistroPagos |
||||
fields = ['monto_pagado', 'monto_extra', 'observaciones'] |
||||
widgets = { |
||||
'monto_pagado': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'Monto pagado'}), |
||||
'monto_extra': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'Monto extra'}), |
||||
'observaciones': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Observaciones'}), |
||||
} |
||||
@ -0,0 +1,39 @@ |
||||
# Generated by Django 5.1.4 on 2024-12-19 04:48 |
||||
|
||||
import django.db.models.deletion |
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
initial = True |
||||
|
||||
dependencies = [ |
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.CreateModel( |
||||
name='RegistrarCompra', |
||||
fields=[ |
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('nombre_compra', models.CharField(max_length=100)), |
||||
('monto_pago', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), |
||||
('observacion', models.TextField(blank=True)), |
||||
('estado', models.CharField(choices=[('Pendiente', 'Pendiente'), ('Progreso', 'En Progreso'), ('Completado', 'Completado')], default='Pendiente', max_length=20)), |
||||
], |
||||
), |
||||
migrations.CreateModel( |
||||
name='RegistroPagos', |
||||
fields=[ |
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('fecha_pago', models.DateField(auto_now_add=True)), |
||||
('monto_pagado', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), |
||||
('monto_extra', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), |
||||
('observaciones', models.TextField(blank=True)), |
||||
('registro_compra', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pagos', to='tasks.registrarcompra')), |
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), |
||||
], |
||||
), |
||||
] |
||||
@ -0,0 +1,17 @@ |
||||
# Generated by Django 5.1.4 on 2024-12-19 05:30 |
||||
|
||||
from django.db import migrations |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('tasks', '0001_initial'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.RemoveField( |
||||
model_name='registrarcompra', |
||||
name='estado', |
||||
), |
||||
] |
||||
@ -0,0 +1,36 @@ |
||||
# Generated by Django 5.1.4 on 2024-12-19 06:35 |
||||
|
||||
import django.db.models.deletion |
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('tasks', '0002_remove_registrarcompra_estado'), |
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AddField( |
||||
model_name='registrarcompra', |
||||
name='user_compra', |
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='registrarcompra', |
||||
name='monto_pago', |
||||
field=models.DecimalField(decimal_places=2, max_digits=10), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='registropagos', |
||||
name='monto_extra', |
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='registropagos', |
||||
name='monto_pagado', |
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True), |
||||
), |
||||
] |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,46 @@ |
||||
from django.db import models |
||||
from django.contrib.auth.models import User |
||||
from django.db.models import Sum |
||||
|
||||
# Create your models here. |
||||
class RegistrarCompra(models.Model): |
||||
nombre_compra = models.CharField(max_length=100) |
||||
monto_pago = models.DecimalField(max_digits=10, decimal_places=2) |
||||
observacion = models.TextField(blank=True) |
||||
user_compra = models.ForeignKey(User, on_delete=models.CASCADE, null=True) |
||||
|
||||
def calcular_estado(self): |
||||
# Usamos 'pagos' en lugar de 'registropagos_set' y sumamos 'monto_extra' |
||||
pagos = self.pagos.aggregate(total_pagado=Sum('monto_pagado'))['total_pagado'] or 0 |
||||
pagos_extra = self.pagos.aggregate(total_extra=Sum('monto_extra'))['total_extra'] or 0 |
||||
total_pagado = pagos + pagos_extra |
||||
|
||||
if total_pagado == 0: |
||||
return "Pendiente" |
||||
elif total_pagado < self.monto_pago: |
||||
return "Pagado Parcialmente" |
||||
else: |
||||
return "Completado" |
||||
|
||||
def calcular_restante(self): |
||||
# Usamos 'pagos' en lugar de 'registropagos_set' y sumamos 'monto_extra' |
||||
pagos = self.pagos.aggregate(total_pagado=Sum('monto_pagado'))['total_pagado'] or 0 |
||||
pagos_extra = self.pagos.aggregate(total_extra=Sum('monto_extra'))['total_extra'] or 0 |
||||
total_pagado = pagos + pagos_extra |
||||
return max(self.monto_pago - total_pagado, 0) |
||||
|
||||
|
||||
class RegistroPagos(models.Model): |
||||
user = models.ForeignKey(User, on_delete=models.CASCADE) |
||||
registro_compra = models.ForeignKey(RegistrarCompra, on_delete=models.CASCADE, related_name='pagos') # Aquí agregamos `related_name` |
||||
fecha_pago = models.DateField(auto_now_add=True) |
||||
monto_pagado = models.DecimalField(max_digits=10, decimal_places=2, null=True) |
||||
monto_extra = models.DecimalField(max_digits=10, decimal_places=2, null=True) |
||||
observaciones = models.TextField(blank=True) |
||||
|
||||
def __str__(self): |
||||
return f"Pago de {self.user.username} - {self.monto_pagado} (+{self.monto_extra})" |
||||
|
||||
@property |
||||
def total_pagado(self): |
||||
return self.monto_pagado + self.monto_extra |
||||
@ -0,0 +1,78 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="es"> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>{% block title %}Gestor de Pagos{% endblock %}</title> |
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> |
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet"> |
||||
</head> |
||||
<style> |
||||
.badge-success { |
||||
background-color: #28a745; |
||||
} |
||||
.badge-warning { |
||||
background-color: #ffc107; |
||||
} |
||||
.badge-danger { |
||||
background-color: #dc3545; |
||||
} |
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) { |
||||
background-color: #f9f9f9; |
||||
} |
||||
.btn-primary { |
||||
background-color: #007bff; |
||||
border-color: #007bff; |
||||
} |
||||
.btn-secondary { |
||||
background-color: #6c757d; |
||||
border-color: #6c757d; |
||||
} |
||||
.btn-sm { |
||||
font-size: 0.875rem; |
||||
} |
||||
html, body { |
||||
height: 100%; |
||||
margin: 0; |
||||
} |
||||
.container { |
||||
min-height: 80%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
.footer { |
||||
margin-top: auto; |
||||
} |
||||
</style> |
||||
<body> |
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> |
||||
<div class="container-fluid"> |
||||
<a class="navbar-brand" href="{% url 'signin' %}">Gestor de Pagos</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"> |
||||
{% if user.is_authenticated %} |
||||
<li class="nav-item"><a class="nav-link" href="{% url 'lista_compras' %}">Nueva Compra</a></li> |
||||
<li class="nav-item"><a class="nav-link" href="{% url 'signout' %}">Cerrar Sesión</a></li> |
||||
{% else %} |
||||
<li class="nav-item"><a class="nav-link" href="{% url 'signin' %}">Inicio de Sesión</a></li> |
||||
<li class="nav-item"><a class="nav-link" href="{% url 'signup' %}">Registrarse</a></li> |
||||
|
||||
{% endif %} |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
</nav> |
||||
<div class="container mt-4"> |
||||
{% block content %} |
||||
{% endblock %} |
||||
</div> |
||||
<footer class="bg-dark text-white text-center py-3 mt-4"> |
||||
<p>© 2024 Gestor de Pagos</p> |
||||
</footer> |
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,29 @@ |
||||
{% extends 'base.html' %} |
||||
|
||||
{% block content %} |
||||
<div class="container"> |
||||
<h2>Registrar Nueva Compra</h2> |
||||
|
||||
<form method="POST"> |
||||
{% csrf_token %} |
||||
{{ form.as_p }} |
||||
|
||||
<div class="d-flex justify-content-end mt-3"> |
||||
<a href="{% url 'lista_compras' %}" class="btn btn-secondary btn-sm me-2"> |
||||
Volver a la Lista |
||||
</a> |
||||
<button type="submit" class="btn btn-success btn-sm"> |
||||
Guardar |
||||
</button> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
{% endblock %} |
||||
|
||||
{% block footer %} |
||||
<footer class="footer mt-auto py-3 bg-dark text-white"> |
||||
<div class="container"> |
||||
<span class="text-muted">© 2024 Mi Aplicación de Compras. Todos los derechos reservados.</span> |
||||
</div> |
||||
</footer> |
||||
{% endblock %} |
||||
@ -0,0 +1,82 @@ |
||||
{% extends 'base.html' %} |
||||
|
||||
{% block content %} |
||||
<div class="container"> |
||||
<h2>Detalle de Compra: {{ compra.nombre_compra }}</h2> |
||||
|
||||
<!-- Contenedor con tabla responsiva --> |
||||
<div class="table-responsive"> |
||||
<table class="table table-bordered"> |
||||
<thead> |
||||
<tr> |
||||
<th>Fecha de Pago</th> |
||||
<th>Observaciones</th> |
||||
<th>Monto Extra</th> |
||||
<th>Monto Pagado</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{% for pago in pagos %} |
||||
<tr> |
||||
<td>{{ pago.fecha_pago }}</td> |
||||
<td>{{ pago.observaciones }}</td> |
||||
<td>${{ pago.monto_extra }}</td> |
||||
<td>${{ pago.monto_pagado }}</td> |
||||
</tr> |
||||
{% empty %} |
||||
<tr> |
||||
<td colspan="4" class="text-center">No se han registrado pagos para esta compra.</td> |
||||
</tr> |
||||
{% endfor %} |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
|
||||
<!-- Botones de acción --> |
||||
<div class="d-flex justify-content-end mt-3"> |
||||
<div class="dropdown"> |
||||
<button class="btn btn-dark btn-sm me-2 dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> |
||||
Descargar Reportes |
||||
</button> |
||||
<ul class="dropdown-menu dropdown-menu-dark"> |
||||
<li> <a href="{% url 'reporte_detalle_compra' compra.id %}" class="dropdown-item"> Generar PDF </a></li> |
||||
<li> <a href="{% url 'exportar_excel' compra.id %}" class="dropdown-item">Generar Excel</a> </li> |
||||
<li> <a href="{% url 'exportar_csv' compra.id %}" class="dropdown-item">Generar CSV</a> </li> |
||||
</ul> |
||||
</div> |
||||
<a href="{% url 'lista_compras' %}" class="btn btn-secondary btn-sm me-2"> |
||||
Volver a la Lista |
||||
</a> |
||||
<a href="{% url 'registrar_pago' compra.id %}" class="btn btn-primary btn-sm me-2"> |
||||
Registrar Pago |
||||
</a> |
||||
|
||||
</div> |
||||
<!-- Información de pago --> |
||||
<div class="mt-3"> |
||||
<h5>Monto Total: ${{ compra.monto_pago }}</h5> |
||||
<h5>Total Pagado: ${{ total_pagado }}</h5> |
||||
<h5>Restante: ${{ restante }}</h5> |
||||
<h5>Estado: |
||||
<span class="badge |
||||
{% if estado == 'Completado' %} |
||||
badge-success |
||||
{% elif estado == 'Pagado Parcialmente' %} |
||||
badge-warning |
||||
{% else %} |
||||
badge-danger |
||||
{% endif %}"> |
||||
{{ estado }} |
||||
</span> |
||||
</h5> |
||||
</div> |
||||
</div> |
||||
{% endblock %} |
||||
|
||||
{% block footer %} |
||||
<footer class="footer mt-auto py-3 bg-dark text-white"> |
||||
<div class="container"> |
||||
<span class="text-muted">© 2024 Mi Aplicación de Compras. Todos los derechos reservados.</span> |
||||
</div> |
||||
</footer> |
||||
{% endblock %} |
||||
@ -0,0 +1,55 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="es"> |
||||
<head> |
||||
<style> |
||||
body { font-family: 'Helvetica', sans-serif; margin: 20px; color: #333; } |
||||
.header { text-align: center; margin-bottom: 30px; } |
||||
.header h1 { font-size: 24px; color: #555; } |
||||
.details { margin-bottom: 20px; } |
||||
.details p { margin: 5px 0; } |
||||
table { width: 100%; border-collapse: collapse; } |
||||
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; } |
||||
th { background-color: #007bff; color: white; } |
||||
.footer { text-align: center; margin-top: 30px; font-size: 12px; color: #777; } |
||||
td { |
||||
word-wrap: break-word; /* Ajusta el texto al ancho de la celda */ |
||||
white-space: pre-wrap; /* Respeta saltos de línea */ |
||||
max-width: 300px; /* Limita el ancho de la celda */ |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
<div class="header"> |
||||
<h1>Reporte de Detalle de Compra</h1> |
||||
</div> |
||||
<div class="details"> |
||||
<p><strong>Nombre de Compra:</strong> {{ compra.nombre_compra }}</p> |
||||
<p><strong>Monto Total:</strong> ${{ compra.monto_pago }}</p> |
||||
<p><strong>Total Pagado:</strong> ${{ total_pagado }}</p> |
||||
<p><strong>Restante:</strong> ${{ restante }}</p> |
||||
</div> |
||||
<table> |
||||
<thead> |
||||
<tr> |
||||
<th>Fecha</th> |
||||
<th>Monto Pagado</th> |
||||
<th>Monto Extra</th> |
||||
<th>Observaciones</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{% for pago in pagos %} |
||||
<tr> |
||||
<td>{{ pago.fecha_pago }}</td> |
||||
<td>${{ pago.monto_pagado }}</td> |
||||
<td>${{ pago.monto_extra }}</td> |
||||
<td>{{ pago.observaciones|default:"Sin observaciones" }}</td> |
||||
</tr> |
||||
{% endfor %} |
||||
</tbody> |
||||
</table> |
||||
<div class="footer"> |
||||
<p>Reporte generado automáticamente | © 2024 Gestor de Pagos</p> |
||||
</div> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,89 @@ |
||||
{% extends "Base.html" %} |
||||
|
||||
{% block content %} |
||||
{% if user.is_authenticated %} |
||||
|
||||
{% else %} |
||||
<style> |
||||
body { |
||||
background: #2c2f36; /* Color de fondo oscuro */ |
||||
color: #fff; /* Texto en blanco para contraste */ |
||||
} |
||||
.card { |
||||
background-color: #ffffff; /* Fondo blanco para el formulario */ |
||||
border-radius: 12px; |
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
||||
} |
||||
.card-body { |
||||
padding: 30px; |
||||
} |
||||
.form-label { |
||||
color: #333; /* Color de las etiquetas */ |
||||
} |
||||
.btn-primary { |
||||
background-color: #007bff; /* Botón azul */ |
||||
border-color: #007bff; |
||||
} |
||||
.btn-primary:hover { |
||||
background-color: #0056b3; /* Color al pasar el ratón */ |
||||
border-color: #0056b3; |
||||
} |
||||
.alert { |
||||
background-color: #f8d7da; /* Fondo rojo suave para los errores */ |
||||
border-color: #f5c6cb; |
||||
} |
||||
.alert p { |
||||
margin-bottom: 0; |
||||
} |
||||
</style> |
||||
<div class="container-fluid p-0" style="height: 100vh; background-color: #2c2f36;"> |
||||
<div class="d-flex justify-content-center align-items-center" style="height: 100vh;"> |
||||
<div class="card shadow-lg p-4" style="max-width: 400px; border-radius: 12px; background-color: #ffffff;"> |
||||
<div class="card-body"> |
||||
<h3 class="text-center mb-4" style="color: #333;">Iniciar Sesión</h3> |
||||
|
||||
<form method="post"> |
||||
{% csrf_token %} |
||||
<div class="mb-3"> |
||||
<label for="username" class="form-label" style="color: #333;">Nombre de Usuario</label> |
||||
<input type="text" class="form-control" id="username" name="username" required |
||||
style="border-radius: 10px; box-shadow: none; transition: all 0.3s ease;"> |
||||
</div> |
||||
|
||||
<div class="mb-3"> |
||||
<label for="password" class="form-label" style="color: #333;">Contraseña</label> |
||||
<input type="password" class="form-control" id="password" name="password" required |
||||
style="border-radius: 10px; box-shadow: none; transition: all 0.3s ease;"> |
||||
</div> |
||||
|
||||
<div class="mb-3 form-check"> |
||||
<input type="checkbox" class="form-check-input" id="rememberMe"> |
||||
<label class="form-check-label" for="rememberMe" style="color: #333;">Recordarme</label> |
||||
</div> |
||||
|
||||
<button type="submit" class="btn btn-dark w-100" style="border-radius: 10px; font-size: 16px; letter-spacing: 1px;"> |
||||
Iniciar Sesión |
||||
</button> |
||||
</form> |
||||
|
||||
<div class="text-center mt-3"> |
||||
<a href="{% url 'password_reset' %}" class="text-muted" style="font-size: 14px;">¿Olvidaste tu contraseña?</a> |
||||
</div> |
||||
<div class="text-center mt-3"> |
||||
<a href="{% url 'signup' %}" class="text-muted" style="font-size: 14px;">Registrar</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endif %} |
||||
|
||||
{% endblock %} |
||||
{% block footer %} |
||||
<footer class="footer mt-auto py-3 bg-dark text-white"> |
||||
<div class="container"> |
||||
<span class="text-muted">© 2024 Mi Aplicación. Todos los derechos reservados.</span> |
||||
</div> |
||||
</footer> |
||||
{% endblock %} |
||||
|
||||
@ -0,0 +1,64 @@ |
||||
{% extends 'base.html' %} |
||||
|
||||
{% block content %} |
||||
<div class="container"> |
||||
<h2>Lista de Compras</h2> |
||||
|
||||
<!-- Contenedor de la tabla responsiva --> |
||||
<div class="table-responsive"> |
||||
<table class="table table-striped table-hover"> |
||||
<thead class="table-dark"> |
||||
<tr> |
||||
<th>Nombre de Compra</th> |
||||
<th>Monto Total</th> |
||||
<th>Estado</th> |
||||
<th>Restante</th> |
||||
<th>Opciones</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{% for item in compras_con_estado %} |
||||
<tr> |
||||
<td>{{ item.compra.nombre_compra }}</td> |
||||
<td>${{ item.compra.monto_pago }}</td> |
||||
<td> |
||||
<span class="badge |
||||
{% if item.estado == 'Completado' %} |
||||
badge-success |
||||
{% elif item.estado == 'Pagado Parcialmente' %} |
||||
badge-warning |
||||
{% else %} |
||||
badge-danger |
||||
{% endif %}"> |
||||
{{ item.estado }} |
||||
</span> |
||||
</td> |
||||
<td>${{ item.restante }}</td> |
||||
<td> |
||||
<a href="{% url 'detalle_compra' item.compra.id %}" class="btn btn-sm btn-primary">Ver Detalle</a> |
||||
</td> |
||||
</tr> |
||||
{% empty %} |
||||
<tr> |
||||
<td colspan="5" class="text-center">No hay compras registradas.</td> |
||||
</tr> |
||||
{% endfor %} |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
|
||||
<!-- Botón flotante para crear nueva compra --> |
||||
<a href="{% url 'crear_compra' %}" class="btn btn-success btn-lg rounded-circle position-fixed bottom-0 end-0 m-3"> |
||||
<i class="fas fa-plus"></i> |
||||
</a> |
||||
</div> |
||||
|
||||
{% endblock %} |
||||
|
||||
{% block footer %} |
||||
<footer class="footer mt-auto py-3 bg-dark text-white"> |
||||
<div class="container"> |
||||
<span class="text-muted">© 2024 Mi Aplicación de Compras. Todos los derechos reservados.</span> |
||||
</div> |
||||
</footer> |
||||
{% endblock %} |
||||
@ -0,0 +1,29 @@ |
||||
{% extends 'base.html' %} |
||||
|
||||
{% block content %} |
||||
<div class="container"> |
||||
<h2>Registrar Pago para: {{ compra.nombre_compra }}</h2> |
||||
|
||||
<form method="POST"> |
||||
{% csrf_token %} |
||||
{{ form.as_p }} |
||||
|
||||
<div class="d-flex justify-content-end mt-3"> |
||||
<a href="{% url 'detalle_compra' compra.id %}" class="btn btn-secondary btn-sm me-2"> |
||||
Volver al Detalle de la Compra |
||||
</a> |
||||
<button type="submit" class="btn btn-success btn-sm"> |
||||
Registrar Pago |
||||
</button> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
{% endblock %} |
||||
|
||||
{% block footer %} |
||||
<footer class="footer mt-auto py-3 bg-dark text-white"> |
||||
<div class="container"> |
||||
<span class="text-muted">© 2024 Mi Aplicación de Compras. Todos los derechos reservados.</span> |
||||
</div> |
||||
</footer> |
||||
{% endblock %} |
||||
@ -0,0 +1,108 @@ |
||||
{% extends "Base.html" %} |
||||
|
||||
{% block content %} |
||||
|
||||
<style> |
||||
body { |
||||
background: #2c2f36; /* Color de fondo oscuro */ |
||||
color: #fff; /* Texto en blanco para contraste */ |
||||
} |
||||
.card { |
||||
background-color: #ffffff; /* Fondo blanco para el formulario */ |
||||
border-radius: 12px; |
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
||||
} |
||||
.card-body { |
||||
padding: 30px; |
||||
} |
||||
.form-label { |
||||
color: #333; /* Color de las etiquetas */ |
||||
} |
||||
.btn-primary { |
||||
background-color: #007bff; /* Botón azul */ |
||||
border-color: #007bff; |
||||
} |
||||
.btn-primary:hover { |
||||
background-color: #0056b3; /* Color al pasar el ratón */ |
||||
border-color: #0056b3; |
||||
} |
||||
.alert { |
||||
background-color: #f8d7da; /* Fondo rojo suave para los errores */ |
||||
border-color: #f5c6cb; |
||||
} |
||||
.alert p { |
||||
margin-bottom: 0; |
||||
} |
||||
</style> |
||||
|
||||
<div class="container"> |
||||
<div class="row justify-content-center mt-5"> |
||||
<div class="col-md-6"> |
||||
<div class="card shadow-lg"> |
||||
<div class="card-body"> |
||||
<h2 class="text-center mb-4" style="color: #333;">Registrar Nueva Cuenta</h2> |
||||
|
||||
<form method="POST"> |
||||
{% csrf_token %} |
||||
|
||||
<div class="form-group mb-3"> |
||||
<label for="username" class="form-label">Nombre de Usuario</label> |
||||
<input type="text" class="form-control" id="username" name="username" value="{{ form.username.value }}"> |
||||
{% if form.username.errors %} |
||||
<div class="alert alert-danger mt-2"> |
||||
{% for error in form.username.errors %} |
||||
<p>{{ error }}</p> |
||||
{% endfor %} |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
<div class="form-group mb-3"> |
||||
<label for="email" class="form-label">Correo Electrónico</label> |
||||
<input type="email" class="form-control" id="email" name="email" value="{{ form.email.value }}"> |
||||
{% if form.email.errors %} |
||||
<div class="alert alert-danger mt-2"> |
||||
{% for error in form.email.errors %} |
||||
<p>{{ error }}</p> |
||||
{% endfor %} |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
<div class="form-group mb-3"> |
||||
<label for="password1" class="form-label">Contraseña</label> |
||||
<input type="password" class="form-control" id="password1" name="password1" value="{{ form.password1.value }}"> |
||||
{% if form.password1.errors %} |
||||
<div class="alert alert-danger mt-2"> |
||||
{% for error in form.password1.errors %} |
||||
<p>{{ error }}</p> |
||||
{% endfor %} |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
<div class="form-group mb-3"> |
||||
<label for="password2" class="form-label">Confirmar Contraseña</label> |
||||
<input type="password" class="form-control" id="password2" name="password2" value="{{ form.password2.value }}"> |
||||
{% if form.password2.errors %} |
||||
<div class="alert alert-danger mt-2"> |
||||
{% for error in form.password2.errors %} |
||||
<p>{{ error }}</p> |
||||
{% endfor %} |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
<div class="text-center"> |
||||
<button type="submit" class="btn btn-primary w-100">Registrar</button> |
||||
</div> |
||||
</form> |
||||
<div class="text-center mt-3"> |
||||
<a href="{% url 'signin' %}" class="text-muted" style="font-size: 14px;">¿Ya tienes cuenta? Inicia sesión</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endblock %} |
||||
@ -0,0 +1,3 @@ |
||||
from django.test import TestCase |
||||
|
||||
# Create your tests here. |
||||
@ -0,0 +1,245 @@ |
||||
from django.shortcuts import render,redirect,get_object_or_404 |
||||
from django.http import HttpResponse |
||||
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm |
||||
from django.contrib.auth import login, logout, authenticate |
||||
from django.contrib.auth.models import User |
||||
from django.contrib.auth.decorators import login_required |
||||
from .models import RegistrarCompra, RegistroPagos |
||||
from .forms import RegistrarCompraForm, RegistroPagosForm |
||||
from django.template.loader import get_template |
||||
from xhtml2pdf import pisa |
||||
import openpyxl |
||||
from django import forms |
||||
import csv |
||||
|
||||
|
||||
|
||||
# Create your views here. |
||||
def base(request): |
||||
return render(request, 'index.html') |
||||
|
||||
class CustomUserCreationForm(UserCreationForm): |
||||
email = forms.EmailField(required=True, help_text="Introduce un correo electrónico válido.") |
||||
|
||||
class Meta: |
||||
model = User |
||||
fields = ('username', 'email', 'password1', 'password2') |
||||
|
||||
def save(self, commit=True): |
||||
user = super().save(commit=False) |
||||
user.email = self.cleaned_data["email"] |
||||
if commit: |
||||
user.save() |
||||
return user |
||||
|
||||
def signup(request): |
||||
if request.method == 'POST': |
||||
form = CustomUserCreationForm(request.POST) |
||||
|
||||
# Verificamos si el formulario es válido |
||||
if form.is_valid(): |
||||
# Guardamos el usuario si todo está bien |
||||
form.save() |
||||
|
||||
# Mensaje de éxito |
||||
return redirect('signin') # Redirige a la página de inicio de sesión |
||||
else: |
||||
# Si el formulario no es válido, mostramos los errores en los campos específicos |
||||
if 'username' not in form.cleaned_data: |
||||
form.add_error('username', 'El nombre de usuario es obligatorio.') |
||||
if 'email' not in form.cleaned_data: |
||||
form.add_error('email', 'El correo electrónico es obligatorio.') |
||||
if 'password1' not in form.cleaned_data: |
||||
form.add_error('password1', 'La contraseña es obligatoria.') |
||||
if 'password2' not in form.cleaned_data: |
||||
form.add_error('password2', 'Debes confirmar la contraseña.') |
||||
|
||||
return render(request, 'singup.html', {'form': form}) |
||||
|
||||
else: |
||||
# Si es un GET, mostramos un formulario vacío |
||||
form = CustomUserCreationForm() |
||||
return render(request, 'singup.html', {'form': form}) |
||||
|
||||
def signin(request): |
||||
if request.method == 'GET': |
||||
return render ( request, 'index.html',{ |
||||
'form': AuthenticationForm |
||||
}) |
||||
else: |
||||
user=authenticate(request, username=request.POST['username'], password=request.POST['password']) |
||||
if user is None: |
||||
return render ( request, 'index.html',{ |
||||
'form': AuthenticationForm , |
||||
'error': 'Username or password is incorrect' |
||||
}) |
||||
else: |
||||
login(request, user) |
||||
return redirect('lista_compras') |
||||
|
||||
def signout(request): |
||||
logout(request) |
||||
return redirect('signin') |
||||
|
||||
|
||||
@login_required |
||||
def crear_compra(request): |
||||
if request.method == 'POST': |
||||
form = RegistrarCompraForm(request.POST) |
||||
if form.is_valid(): |
||||
# Guardamos el formulario y asignamos el usuario autenticado al campo 'user_compra' |
||||
compra = form.save(commit=False) # No guardamos todavía en la base de datos |
||||
compra.user_compra = request.user # Asignamos el usuario autenticado |
||||
compra.save() # Guardamos la compra en la base de datos |
||||
return redirect('lista_compras') # Redirige a una vista que liste las compras |
||||
else: |
||||
form = RegistrarCompraForm() |
||||
return render(request, 'crear_compra.html', {'form': form}) |
||||
|
||||
|
||||
@login_required |
||||
def registrar_pago(request, compra_id): |
||||
compra = get_object_or_404(RegistrarCompra, id=compra_id, user_compra=request.user) # Filtrar por el usuario |
||||
|
||||
# Calcular el total pagado actual (monto_pagado + monto_extra) |
||||
pagos = compra.pagos.all() |
||||
total_pagado = sum(pago.monto_pagado + pago.monto_extra for pago in pagos) |
||||
|
||||
# Calcular el monto restante de la compra |
||||
restante = compra.monto_pago - total_pagado |
||||
|
||||
if request.method == 'POST': |
||||
form = RegistroPagosForm(request.POST) |
||||
if form.is_valid(): |
||||
pago = form.save(commit=False) |
||||
|
||||
# Verificar si el nuevo pago no excede el monto restante |
||||
total_nuevo_pago = pago.monto_pagado + pago.monto_extra |
||||
if total_pagado + total_nuevo_pago > compra.monto_pago: |
||||
form.add_error(None, "El pago total no puede superar el monto de la compra.") |
||||
else: |
||||
pago.registro_compra = compra # Relacionar con la compra específica |
||||
pago.user = request.user # Asignar el usuario autenticado |
||||
pago.save() |
||||
return redirect('detalle_compra', compra_id=compra.id) # Redirige a la vista de detalle de la compra |
||||
else: |
||||
form = RegistroPagosForm() |
||||
|
||||
return render(request, 'registrar_pago.html', {'form': form, 'compra': compra, 'restante': restante}) |
||||
|
||||
@login_required |
||||
def detalle_compra(request, compra_id): |
||||
compra = get_object_or_404(RegistrarCompra, id=compra_id, user_compra=request.user) # Filtrar por el usuario |
||||
pagos = compra.pagos.all() # Obtén todos los pagos relacionados con esta compra |
||||
|
||||
# Calcular el total pagado (monto_pagado + monto_extra) |
||||
total_pagado = sum(pago.monto_pagado + pago.monto_extra for pago in pagos) |
||||
|
||||
# Calcular el monto restante |
||||
restante = max(0, compra.monto_pago - total_pagado) |
||||
|
||||
# Calcular el estado |
||||
estado = compra.calcular_estado() # Utilizamos el método que definimos en el modelo |
||||
|
||||
context = { |
||||
'compra': compra, |
||||
'pagos': pagos, |
||||
'total_pagado': total_pagado, |
||||
'restante': restante, |
||||
'estado': estado, |
||||
} |
||||
|
||||
return render(request, 'detalle_compra.html', context) |
||||
|
||||
@login_required |
||||
def lista_compras(request): |
||||
compras = RegistrarCompra.objects.filter(user_compra=request.user) # Filtrar por el usuario actual |
||||
|
||||
compras_con_estado = [ |
||||
{ |
||||
'compra': compra, |
||||
'estado': compra.calcular_estado(), # Calcula el estado de la compra |
||||
'restante': compra.calcular_restante(), # Calcula el monto restante |
||||
} |
||||
for compra in compras |
||||
] |
||||
|
||||
return render(request, 'lista_compras.html', {'compras_con_estado': compras_con_estado}) |
||||
|
||||
def generar_pdf(template_src, context_dict={}): |
||||
template = get_template(template_src) |
||||
html = template.render(context_dict) |
||||
response = HttpResponse(content_type='application/pdf') |
||||
response['Content-Disposition'] = 'inline; filename="detalle_compra.pdf"' |
||||
pisa_status = pisa.CreatePDF(html, dest=response) |
||||
if pisa_status.err: |
||||
return HttpResponse('Error al generar el PDF', content_type='text/plain') |
||||
return response |
||||
|
||||
|
||||
def reporte_detalle_compra(request, compra_id): |
||||
compra = get_object_or_404(RegistrarCompra, id=compra_id) |
||||
pagos = compra.pagos.all() |
||||
total_pagado = sum(pago.total_pagado for pago in pagos) |
||||
restante = max(0, compra.monto_pago - total_pagado) |
||||
|
||||
context = { |
||||
'compra': compra, |
||||
'pagos': pagos, |
||||
'total_pagado': total_pagado, |
||||
'restante': restante, |
||||
} |
||||
return generar_pdf('detalle_compra_pdf.html', context) |
||||
|
||||
|
||||
|
||||
def exportar_excel_detalle_compra(request, compra_id): |
||||
compra = get_object_or_404(RegistrarCompra, id=compra_id) |
||||
pagos = compra.pagos.all() |
||||
|
||||
# Crear el archivo de Excel |
||||
wb = openpyxl.Workbook() |
||||
ws = wb.active |
||||
ws.title = "Detalle de Compra" |
||||
|
||||
# Escribir encabezados |
||||
ws.append(["Fecha de Pago", "Monto Pagado", "Monto Extra", "Observaciones"]) |
||||
|
||||
# Agregar los datos de los pagos |
||||
for pago in pagos: |
||||
ws.append([ |
||||
pago.fecha_pago, |
||||
float(pago.monto_pagado), |
||||
float(pago.monto_extra), |
||||
pago.observaciones, |
||||
]) |
||||
|
||||
# Configurar la respuesta HTTP |
||||
response = HttpResponse(content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") |
||||
response['Content-Disposition'] = f'attachment; filename="detalle_compra_{compra.id}.xlsx"' |
||||
|
||||
wb.save(response) |
||||
return response |
||||
|
||||
def exportar_csv_detalle_compra(request, compra_id): |
||||
compra = get_object_or_404(RegistrarCompra, id=compra_id) |
||||
pagos = compra.pagos.all() |
||||
|
||||
# Configurar la respuesta HTTP |
||||
response = HttpResponse(content_type="text/csv") |
||||
response['Content-Disposition'] = f'attachment; filename="detalle_compra_{compra.id}.csv"' |
||||
|
||||
# Crear el escritor CSV |
||||
writer = csv.writer(response) |
||||
writer.writerow(["Fecha de Pago", "Monto Pagado", "Monto Extra", "Observaciones"]) # Encabezados |
||||
|
||||
# Agregar los datos de los pagos |
||||
for pago in pagos: |
||||
writer.writerow([ |
||||
pago.fecha_pago, |
||||
float(pago.monto_pagado), |
||||
float(pago.monto_extra), |
||||
pago.observaciones, |
||||
]) |
||||
|
||||
return response |
||||
Loading…
Reference in new issue