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