diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7275bb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +venv/ diff --git a/RegistroDePagos/__init__.py b/RegistroDePagos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/RegistroDePagos/__pycache__/__init__.cpython-313.pyc b/RegistroDePagos/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..4974ce1 Binary files /dev/null and b/RegistroDePagos/__pycache__/__init__.cpython-313.pyc differ diff --git a/RegistroDePagos/__pycache__/settings.cpython-313.pyc b/RegistroDePagos/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000..d70db28 Binary files /dev/null and b/RegistroDePagos/__pycache__/settings.cpython-313.pyc differ diff --git a/RegistroDePagos/__pycache__/urls.cpython-313.pyc b/RegistroDePagos/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..691027f Binary files /dev/null and b/RegistroDePagos/__pycache__/urls.cpython-313.pyc differ diff --git a/RegistroDePagos/__pycache__/wsgi.cpython-313.pyc b/RegistroDePagos/__pycache__/wsgi.cpython-313.pyc new file mode 100644 index 0000000..2ea461f Binary files /dev/null and b/RegistroDePagos/__pycache__/wsgi.cpython-313.pyc differ diff --git a/RegistroDePagos/asgi.py b/RegistroDePagos/asgi.py new file mode 100644 index 0000000..c361b45 --- /dev/null +++ b/RegistroDePagos/asgi.py @@ -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() diff --git a/RegistroDePagos/settings.py b/RegistroDePagos/settings.py new file mode 100644 index 0000000..7adde4b --- /dev/null +++ b/RegistroDePagos/settings.py @@ -0,0 +1,154 @@ +""" +Django settings for RegistroDePagos project. + +Generated by 'django-admin startproject' using Django 5.1.4. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-@ruqv)^ecy3zuj%gn3g%vrl!r!z5r2-ztf+vhplqm9bu@=gy4#' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'tasks', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'RegistroDePagos.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'RegistroDePagos.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', # Motor de MySQL + 'NAME': 'registro_pagos', # Nombre de la base de datos + 'USER': 'root', # Usuario de MySQL + 'PASSWORD': '', # Contraseña de MySQL + 'HOST': 'localhost', # Dirección del servidor (por defecto localhost) + 'PORT': '3306', # Puerto de MySQL (por defecto 3306) + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = 'static/' + +LOGIN_URL='/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = 'registrogestorpagos@gmail.com' +EMAIL_HOST_PASSWORD = 'mlsr wxwu hfey qgiv' +DEFAULT_FROM_EMAIL = 'registrogestorpagos@gmail.com' + + + +LANGUAGE_CODE = 'es-es' +USE_I18N = True +USE_L10N = True +USE_TZ = True +DATE_FORMAT = "d/m/Y" +USE_THOUSAND_SEPARATOR = False +TIME_ZONE = 'America/El_Salvador' + + +SESSION_COOKIE_AGE = 60 * 15 +SESSION_SAVE_EVERY_REQUEST = True +SESSION_EXPIRE_AT_BROWSER_CLOSE = True \ No newline at end of file diff --git a/RegistroDePagos/urls.py b/RegistroDePagos/urls.py new file mode 100644 index 0000000..339f2a2 --- /dev/null +++ b/RegistroDePagos/urls.py @@ -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//pago/', views.registrar_pago, name='registrar_pago'), + path('compras//', views.detalle_compra, name='detalle_compra'), + path('compras//pdf/', views.reporte_detalle_compra, name='reporte_detalle_compra'), + path('compras//excel/', views.exportar_excel_detalle_compra, name='exportar_excel'), + path('compras//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///', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), + path('password_reset_complete/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), +] diff --git a/RegistroDePagos/wsgi.py b/RegistroDePagos/wsgi.py new file mode 100644 index 0000000..1088ed5 --- /dev/null +++ b/RegistroDePagos/wsgi.py @@ -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() diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..69e9f5b --- /dev/null +++ b/manage.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..72a8ffe --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/tasks/__init__.py b/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/__pycache__/__init__.cpython-313.pyc b/tasks/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..1615cce Binary files /dev/null and b/tasks/__pycache__/__init__.cpython-313.pyc differ diff --git a/tasks/__pycache__/admin.cpython-313.pyc b/tasks/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000..ccf3c9b Binary files /dev/null and b/tasks/__pycache__/admin.cpython-313.pyc differ diff --git a/tasks/__pycache__/apps.cpython-313.pyc b/tasks/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000..8c2178d Binary files /dev/null and b/tasks/__pycache__/apps.cpython-313.pyc differ diff --git a/tasks/__pycache__/forms.cpython-313.pyc b/tasks/__pycache__/forms.cpython-313.pyc new file mode 100644 index 0000000..1d4519d Binary files /dev/null and b/tasks/__pycache__/forms.cpython-313.pyc differ diff --git a/tasks/__pycache__/models.cpython-313.pyc b/tasks/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..616bc40 Binary files /dev/null and b/tasks/__pycache__/models.cpython-313.pyc differ diff --git a/tasks/__pycache__/views.cpython-313.pyc b/tasks/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000..4e14925 Binary files /dev/null and b/tasks/__pycache__/views.cpython-313.pyc differ diff --git a/tasks/admin.py b/tasks/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/tasks/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/tasks/apps.py b/tasks/apps.py new file mode 100644 index 0000000..3ff3ab3 --- /dev/null +++ b/tasks/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TasksConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'tasks' diff --git a/tasks/forms.py b/tasks/forms.py new file mode 100644 index 0000000..3944583 --- /dev/null +++ b/tasks/forms.py @@ -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'}), + } \ No newline at end of file diff --git a/tasks/migrations/0001_initial.py b/tasks/migrations/0001_initial.py new file mode 100644 index 0000000..2851130 --- /dev/null +++ b/tasks/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/tasks/migrations/0002_remove_registrarcompra_estado.py b/tasks/migrations/0002_remove_registrarcompra_estado.py new file mode 100644 index 0000000..74d4cc6 --- /dev/null +++ b/tasks/migrations/0002_remove_registrarcompra_estado.py @@ -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', + ), + ] diff --git a/tasks/migrations/0003_registrarcompra_user_compra_and_more.py b/tasks/migrations/0003_registrarcompra_user_compra_and_more.py new file mode 100644 index 0000000..702f86f --- /dev/null +++ b/tasks/migrations/0003_registrarcompra_user_compra_and_more.py @@ -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), + ), + ] diff --git a/tasks/migrations/__init__.py b/tasks/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/migrations/__pycache__/0001_initial.cpython-313.pyc b/tasks/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000..f25ba99 Binary files /dev/null and b/tasks/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/tasks/migrations/__pycache__/0002_remove_registrarcompra_estado.cpython-313.pyc b/tasks/migrations/__pycache__/0002_remove_registrarcompra_estado.cpython-313.pyc new file mode 100644 index 0000000..e2bece8 Binary files /dev/null and b/tasks/migrations/__pycache__/0002_remove_registrarcompra_estado.cpython-313.pyc differ diff --git a/tasks/migrations/__pycache__/0003_registrarcompra_user_compra_and_more.cpython-313.pyc b/tasks/migrations/__pycache__/0003_registrarcompra_user_compra_and_more.cpython-313.pyc new file mode 100644 index 0000000..5fcfc6b Binary files /dev/null and b/tasks/migrations/__pycache__/0003_registrarcompra_user_compra_and_more.cpython-313.pyc differ diff --git a/tasks/migrations/__pycache__/__init__.cpython-313.pyc b/tasks/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..757f3ef Binary files /dev/null and b/tasks/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/tasks/models.py b/tasks/models.py new file mode 100644 index 0000000..0d74c95 --- /dev/null +++ b/tasks/models.py @@ -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 \ No newline at end of file diff --git a/tasks/templates/base.html b/tasks/templates/base.html new file mode 100644 index 0000000..c9b8c60 --- /dev/null +++ b/tasks/templates/base.html @@ -0,0 +1,78 @@ + + + + + + {% block title %}Gestor de Pagos{% endblock %} + + + + + + +
+ {% block content %} + {% endblock %} +
+
+

© 2024 Gestor de Pagos

+
+ + + \ No newline at end of file diff --git a/tasks/templates/crear_compra.html b/tasks/templates/crear_compra.html new file mode 100644 index 0000000..085b4b6 --- /dev/null +++ b/tasks/templates/crear_compra.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Registrar Nueva Compra

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ + Volver a la Lista + + +
+
+
+{% endblock %} + +{% block footer %} +
+
+ © 2024 Mi Aplicación de Compras. Todos los derechos reservados. +
+
+{% endblock %} \ No newline at end of file diff --git a/tasks/templates/detalle_compra.html b/tasks/templates/detalle_compra.html new file mode 100644 index 0000000..6538b9d --- /dev/null +++ b/tasks/templates/detalle_compra.html @@ -0,0 +1,82 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Detalle de Compra: {{ compra.nombre_compra }}

+ + +
+ + + + + + + + + + + {% for pago in pagos %} + + + + + + + {% empty %} + + + + {% endfor %} + +
Fecha de PagoObservacionesMonto ExtraMonto Pagado
{{ pago.fecha_pago }}{{ pago.observaciones }}${{ pago.monto_extra }}${{ pago.monto_pagado }}
No se han registrado pagos para esta compra.
+
+ + + + +
+
Monto Total: ${{ compra.monto_pago }}
+
Total Pagado: ${{ total_pagado }}
+
Restante: ${{ restante }}
+
Estado: + + {{ estado }} + +
+
+
+{% endblock %} + +{% block footer %} +
+
+ © 2024 Mi Aplicación de Compras. Todos los derechos reservados. +
+
+{% endblock %} \ No newline at end of file diff --git a/tasks/templates/detalle_compra_pdf.html b/tasks/templates/detalle_compra_pdf.html new file mode 100644 index 0000000..ea0001e --- /dev/null +++ b/tasks/templates/detalle_compra_pdf.html @@ -0,0 +1,55 @@ + + + + + + +
+

Reporte de Detalle de Compra

+
+
+

Nombre de Compra: {{ compra.nombre_compra }}

+

Monto Total: ${{ compra.monto_pago }}

+

Total Pagado: ${{ total_pagado }}

+

Restante: ${{ restante }}

+
+ + + + + + + + + + + {% for pago in pagos %} + + + + + + + {% endfor %} + +
FechaMonto PagadoMonto ExtraObservaciones
{{ pago.fecha_pago }}${{ pago.monto_pagado }}${{ pago.monto_extra }}{{ pago.observaciones|default:"Sin observaciones" }}
+ + + \ No newline at end of file diff --git a/tasks/templates/index.html b/tasks/templates/index.html new file mode 100644 index 0000000..831580b --- /dev/null +++ b/tasks/templates/index.html @@ -0,0 +1,89 @@ +{% extends "Base.html" %} + +{% block content %} +{% if user.is_authenticated %} + +{% else %} + +
+
+
+
+

Iniciar Sesión

+ +
+ {% csrf_token %} +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ Registrar +
+
+
+
+
+{% endif %} + +{% endblock %} +{% block footer %} +
+
+ © 2024 Mi Aplicación. Todos los derechos reservados. +
+
+{% endblock %} + \ No newline at end of file diff --git a/tasks/templates/lista_compras.html b/tasks/templates/lista_compras.html new file mode 100644 index 0000000..ab95b68 --- /dev/null +++ b/tasks/templates/lista_compras.html @@ -0,0 +1,64 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Lista de Compras

+ + +
+ + + + + + + + + + + + {% for item in compras_con_estado %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
Nombre de CompraMonto TotalEstadoRestanteOpciones
{{ item.compra.nombre_compra }}${{ item.compra.monto_pago }} + + {{ item.estado }} + + ${{ item.restante }} + Ver Detalle +
No hay compras registradas.
+
+ + + + + +
+ +{% endblock %} + +{% block footer %} +
+
+ © 2024 Mi Aplicación de Compras. Todos los derechos reservados. +
+
+{% endblock %} diff --git a/tasks/templates/registrar_pago.html b/tasks/templates/registrar_pago.html new file mode 100644 index 0000000..d44f4a0 --- /dev/null +++ b/tasks/templates/registrar_pago.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Registrar Pago para: {{ compra.nombre_compra }}

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ + Volver al Detalle de la Compra + + +
+
+
+{% endblock %} + +{% block footer %} +
+
+ © 2024 Mi Aplicación de Compras. Todos los derechos reservados. +
+
+{% endblock %} \ No newline at end of file diff --git a/tasks/templates/singnin.html b/tasks/templates/singnin.html new file mode 100644 index 0000000..e69de29 diff --git a/tasks/templates/singup.html b/tasks/templates/singup.html new file mode 100644 index 0000000..cb5661e --- /dev/null +++ b/tasks/templates/singup.html @@ -0,0 +1,108 @@ +{% extends "Base.html" %} + +{% block content %} + + + +
+
+
+
+
+

Registrar Nueva Cuenta

+ +
+ {% csrf_token %} + +
+ + + {% if form.username.errors %} +
+ {% for error in form.username.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+ + + {% if form.email.errors %} +
+ {% for error in form.email.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+ + + {% if form.password1.errors %} +
+ {% for error in form.password1.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+ + + {% if form.password2.errors %} +
+ {% for error in form.password2.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+ +
+ +
+
+ +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/tasks/tests.py b/tasks/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/tasks/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/tasks/views.py b/tasks/views.py new file mode 100644 index 0000000..819fd02 --- /dev/null +++ b/tasks/views.py @@ -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 \ No newline at end of file