diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7275bb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +venv/ diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..5dbde4c --- /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', 'registro.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/registro/__init__.py b/registro/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/registro/__pycache__/__init__.cpython-313.pyc b/registro/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..bef3153 Binary files /dev/null and b/registro/__pycache__/__init__.cpython-313.pyc differ diff --git a/registro/__pycache__/settings.cpython-313.pyc b/registro/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000..76bbfa9 Binary files /dev/null and b/registro/__pycache__/settings.cpython-313.pyc differ diff --git a/registro/__pycache__/urls.cpython-313.pyc b/registro/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..585b9d1 Binary files /dev/null and b/registro/__pycache__/urls.cpython-313.pyc differ diff --git a/registro/__pycache__/wsgi.cpython-313.pyc b/registro/__pycache__/wsgi.cpython-313.pyc new file mode 100644 index 0000000..6ed80cf Binary files /dev/null and b/registro/__pycache__/wsgi.cpython-313.pyc differ diff --git a/registro/asgi.py b/registro/asgi.py new file mode 100644 index 0000000..6e1358b --- /dev/null +++ b/registro/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for registro 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', 'registro.settings') + +application = get_asgi_application() diff --git a/registro/settings.py b/registro/settings.py new file mode 100644 index 0000000..90b57cb --- /dev/null +++ b/registro/settings.py @@ -0,0 +1,154 @@ +""" +Django settings for registro project. + +Generated by 'django-admin startproject' using Django 5.1.5. + +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/ +""" +import os +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-f2t$19_-10d9%01a9e#p17ehiz13e)17232df21+kd8*q9+s&%' + +# 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 = 'registro.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 = 'registro.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', # 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 = 'signin' + +# 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' + +FILE_MAX_SIZE = 910242880 +SUPPORTED_MIME_TYPES = ['image/png', 'image/jpeg', 'application/pdf','video/mp4', 'text/csv', 'text/plain'] +PATH_FILES = os.path.join(os.path.dirname(BASE_DIR), "files/") +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/registro/urls.py b/registro/urls.py new file mode 100644 index 0000000..306129a --- /dev/null +++ b/registro/urls.py @@ -0,0 +1,33 @@ +""" +URL configuration for registro 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 + +urlpatterns = [ + path('admin/', admin.site.urls), + path('',views.signin, name='signin'), + path('signup/',views.signup, name='signup'), + path('cuentas/', views.lista_cuentas, name='lista_cuentas'), + path('cuentas/nueva/', views.registrar_cuenta, name='registrar_cuenta'), + path('signout/',views.signout, name='signout'), + path('movimientos//', views.lista_movimientos, name='lista_movimientos'), + path('movimientos//nuevo', views.registrar_movimiento, name='registrar_movimiento'), + path('movimientos/editar//', views.actualizar_movimiento, name='actualizar_movimiento'), + path('movimientos/excel//', views.exportar_movimientos_excel, name='exportar_movimientos_excel'), + +] diff --git a/registro/wsgi.py b/registro/wsgi.py new file mode 100644 index 0000000..c97078f --- /dev/null +++ b/registro/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for registro 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', 'registro.settings') + +application = get_wsgi_application() 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..72afd37 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..f1e4e44 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..cf0980b 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..f31edbd 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..9b3ee9a 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..1a3e6b2 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..f3529de --- /dev/null +++ b/tasks/forms.py @@ -0,0 +1,36 @@ +from django import forms +from .models import Cuenta, Movimientos, TipoMovimiento + +class RegistroCuentaForm(forms.ModelForm): + class Meta: + model = Cuenta + fields = ['cuenta', 'observacion'] + widgets = { + 'cuenta': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Nombre de la cuenta'}), + 'observacion': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Observaciones'}), + } +class RegistroMovimientosForm(forms.ModelForm): + tipo_movimiento = forms.ModelChoiceField( + queryset=TipoMovimiento.objects.all(), + empty_label="Seleccione un tipo de movimiento", + widget=forms.Select(attrs={'class': 'form-control'}), + label="Tipo de movimiento" + ) + fecha_factura = forms.DateField( + widget=forms.DateInput( + attrs={'type': 'date', 'class': 'form-control'} + ), + label="Fecha de factura", + ) + + class Meta: + model = Movimientos + fields = ['tipo_movimiento','fecha_factura', 'saldo','n_factura', 'observacion','proveedor','responsable_cuenta'] + widgets = { + 'saldo': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'Monto'}), + 'observacion': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Observaciones'}), + 'n_factura': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'Ingrese numero de factura'}), + 'proveedor': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Nombre del proveedor'}), + 'responsable_cuenta': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Nombre del responsable'}), + + } \ 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..3ce1fd7 --- /dev/null +++ b/tasks/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# Generated by Django 5.1.5 on 2025-02-06 13:47 + +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='TipoMovimiento', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tipo_movimiento', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Cuenta', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cuenta', models.CharField(max_length=100)), + ('fecha_creacion', models.DateField(auto_now_add=True)), + ('saldo', models.DecimalField(decimal_places=2, default=0.0, max_digits=15, null=True)), + ('observacion', models.TextField(blank=True)), + ('responsable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Movimientos', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('saldo', models.DecimalField(decimal_places=2, default=0.0, max_digits=15, null=True)), + ('fecha_insersion', models.DateField(auto_now_add=True)), + ('observacion', models.TextField(blank=True)), + ('responsable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('tipo_movimiento', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.tipomovimiento')), + ], + ), + ] diff --git a/tasks/migrations/0002_movimientos_cuentas_alter_cuenta_fecha_creacion_and_more.py b/tasks/migrations/0002_movimientos_cuentas_alter_cuenta_fecha_creacion_and_more.py new file mode 100644 index 0000000..438a569 --- /dev/null +++ b/tasks/migrations/0002_movimientos_cuentas_alter_cuenta_fecha_creacion_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.5 on 2025-02-06 15:50 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='movimientos', + name='cuentas', + field=models.ForeignKey(blank=True, default=1, on_delete=django.db.models.deletion.CASCADE, to='tasks.cuenta'), + preserve_default=False, + ), + migrations.AlterField( + model_name='cuenta', + name='fecha_creacion', + field=models.DateField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='movimientos', + name='fecha_insersion', + field=models.DateField(default=django.utils.timezone.now), + ), + ] diff --git a/tasks/migrations/0003_alter_movimientos_cuentas.py b/tasks/migrations/0003_alter_movimientos_cuentas.py new file mode 100644 index 0000000..a8cf530 --- /dev/null +++ b/tasks/migrations/0003_alter_movimientos_cuentas.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.5 on 2025-02-06 18:13 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0002_movimientos_cuentas_alter_cuenta_fecha_creacion_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='movimientos', + name='cuentas', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.cuenta'), + ), + ] diff --git a/tasks/migrations/0004_movimientos_fecha_factura_movimientos_n_factura_and_more.py b/tasks/migrations/0004_movimientos_fecha_factura_movimientos_n_factura_and_more.py new file mode 100644 index 0000000..d0c91d8 --- /dev/null +++ b/tasks/migrations/0004_movimientos_fecha_factura_movimientos_n_factura_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.1.5 on 2025-02-07 01:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0003_alter_movimientos_cuentas'), + ] + + operations = [ + migrations.AddField( + model_name='movimientos', + name='fecha_factura', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='movimientos', + name='n_factura', + field=models.PositiveIntegerField(default=1), + preserve_default=False, + ), + migrations.AddField( + model_name='movimientos', + name='proveedor', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='movimientos', + name='responsable_cuenta', + field=models.CharField(blank=True, max_length=100), + ), + ] 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..2820e9f Binary files /dev/null and b/tasks/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/tasks/migrations/__pycache__/0002_movimientos_cuentas_alter_cuenta_fecha_creacion_and_more.cpython-313.pyc b/tasks/migrations/__pycache__/0002_movimientos_cuentas_alter_cuenta_fecha_creacion_and_more.cpython-313.pyc new file mode 100644 index 0000000..eb9eb5d Binary files /dev/null and b/tasks/migrations/__pycache__/0002_movimientos_cuentas_alter_cuenta_fecha_creacion_and_more.cpython-313.pyc differ diff --git a/tasks/migrations/__pycache__/0003_alter_movimientos_cuentas.cpython-313.pyc b/tasks/migrations/__pycache__/0003_alter_movimientos_cuentas.cpython-313.pyc new file mode 100644 index 0000000..b128ddc Binary files /dev/null and b/tasks/migrations/__pycache__/0003_alter_movimientos_cuentas.cpython-313.pyc differ diff --git a/tasks/migrations/__pycache__/0004_movimientos_fecha_factura_movimientos_n_factura_and_more.cpython-313.pyc b/tasks/migrations/__pycache__/0004_movimientos_fecha_factura_movimientos_n_factura_and_more.cpython-313.pyc new file mode 100644 index 0000000..15fb798 Binary files /dev/null and b/tasks/migrations/__pycache__/0004_movimientos_fecha_factura_movimientos_n_factura_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..c12a9df 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..71d9868 --- /dev/null +++ b/tasks/models.py @@ -0,0 +1,33 @@ +from django.db import models +from django.contrib.auth.models import User +from django.utils import timezone +from decimal import Decimal + + +# Create your models here. +class TipoMovimiento(models.Model): + tipo_movimiento=models.CharField(max_length=100) + def __str__(self): + return self.tipo_movimiento # Retorna el nombre legible + + +class Cuenta (models.Model): + cuenta=models.CharField(max_length=100) + fecha_creacion=models.DateField(default=timezone.now) + saldo=models.DecimalField(max_digits=15, decimal_places=2, null=True, default=Decimal('0.00')) + responsable = models.ForeignKey(User, on_delete=models.CASCADE) + observacion= models.TextField(blank=True) + def __str__(self): + return self.cuenta + +class Movimientos(models.Model): + responsable = models.ForeignKey(User, on_delete=models.CASCADE) + tipo_movimiento=models.ForeignKey(TipoMovimiento, on_delete=models.CASCADE) + saldo=models.DecimalField(max_digits=15, decimal_places=2, null=True, default=Decimal('0.00')) + fecha_insersion=models.DateField(default=timezone.now) + observacion= models.TextField(blank=True) + cuentas= models.ForeignKey(Cuenta, on_delete=models.CASCADE) + n_factura=models.PositiveIntegerField() + fecha_factura = models.DateField(blank=True) + proveedor= models.CharField(max_length=100, blank=True) + responsable_cuenta=models.CharField(max_length=100, blank=True) \ No newline at end of file diff --git a/tasks/templates/base.html b/tasks/templates/base.html new file mode 100644 index 0000000..266e116 --- /dev/null +++ b/tasks/templates/base.html @@ -0,0 +1,77 @@ + + + + + + {% block title %}Registro{% endblock %} + + + + + + +
+ {% block content %} + {% endblock %} +
+
+

© 2025 registro

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

Registrar Nueva Cuenta

+
+
+
+ {% csrf_token %} + {{ form.as_p }} + + + Cancelar +
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/tasks/templates/index.html b/tasks/templates/index.html new file mode 100644 index 0000000..099b452 --- /dev/null +++ b/tasks/templates/index.html @@ -0,0 +1,80 @@ +{% 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_cuentas.html b/tasks/templates/lista_cuentas.html new file mode 100644 index 0000000..60d4d44 --- /dev/null +++ b/tasks/templates/lista_cuentas.html @@ -0,0 +1,54 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Lista de Cuentas

+
+ {% if es_superusuario %} + + Ver Todas las Cuentas + + + Ver Mis Cuentas + + {% endif %} +
+
+ + + + + + + + + + + + + {% for cuenta in cuentas %} + + + + + + + + + {% endfor %} + +
NombreSaldoResponsableFecha CreaciónObservacionesAcciones
{{ cuenta.cuenta }}{{ cuenta.saldo }}{{ cuenta.responsable.username }}{{ cuenta.fecha_creacion }}{{ cuenta.observacion }} + + Ver Movimientos + + + Exportar a Excel + +
+
+
+ + + + +{% endblock %} diff --git a/tasks/templates/lista_movimientos.html b/tasks/templates/lista_movimientos.html new file mode 100644 index 0000000..eb0511e --- /dev/null +++ b/tasks/templates/lista_movimientos.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Movimientos de la cuenta: {{ cuenta.cuenta }}

+
+ + + + + + + + + + + + + + + {% for movimiento in movimientos %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
TipoMontoFechaObservación# de facturafecha de facturaresponsableAcciones
{{ movimiento.tipo_movimiento }}{{ movimiento.saldo }}{{ movimiento.fecha_insersion }}{{ movimiento.observacion }}{{ movimiento.n_factura }}{{ movimiento.fecha_factura }}{{ movimiento.responsable_cuenta }} + + Editar + +
No hay movimientos registrados.
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/tasks/templates/movimiento.html b/tasks/templates/movimiento.html new file mode 100644 index 0000000..4dcc068 --- /dev/null +++ b/tasks/templates/movimiento.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+

Registrar Nuevo Movimiento

+
+
+
+ {% csrf_token %} + + +
+ + +
+
+ + {{ form.tipo_movimiento }} +
+
+ + {{ form.saldo }} +
+
+ + {{ form.observacion }} +
+
+ + {{ form.n_factura }} +
+ + {{ form.fecha_factura }} +
+ + {{ form.proveedor }} +
+ + {{ form.responsable_cuenta }} +
+ + Cancelar +
+
+
+
+{% endblock %} \ No newline at end of file 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..d74a241 --- /dev/null +++ b/tasks/views.py @@ -0,0 +1,215 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.utils import timezone +from decimal import Decimal +from django.http import HttpResponse +from django.contrib.auth import login, authenticate, logout +from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from .models import Cuenta, Movimientos, TipoMovimiento +from django import forms +import openpyxl +from django.contrib.auth.models import User +from .forms import RegistroCuentaForm, RegistroMovimientosForm + + + +def base(request): + return render(request, 'index.html') + +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_cuentas') + +def signout(request): + logout(request) + return redirect('signin') + +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}) + +@login_required +def registrar_cuenta(request): + if request.method == "POST": + form = RegistroCuentaForm(request.POST) + if form.is_valid(): + cuenta = form.save(commit=False) + cuenta.responsable = request.user + cuenta.save() + return redirect('lista_cuentas') # Cambia el nombre de la vista destino + else: + form = RegistroCuentaForm() + + return render(request, 'cuentas.html', {'form': form}) + +@login_required +def registrar_movimiento(request, cuenta_id): + cuenta = get_object_or_404(Cuenta, id=cuenta_id) # Obtiene la cuenta específica + + if request.method == "POST": + form = RegistroMovimientosForm(request.POST) + if form.is_valid(): + movimiento = form.save(commit=False) + movimiento.cuentas = cuenta # Asigna la cuenta automáticamente + movimiento.responsable = request.user # Asigna el usuario actual + + # Ajusta el saldo según el tipo de movimiento + if movimiento.tipo_movimiento.id == 1: # Supongamos que ID 1 es ingreso + cuenta.saldo += movimiento.saldo + elif movimiento.tipo_movimiento.id == 2: # ID 2 es egreso + cuenta.saldo -= movimiento.saldo + + cuenta.save() + movimiento.save() + return redirect('lista_movimientos', cuenta_id=cuenta.id) + else: + form = RegistroMovimientosForm() + + return render(request, 'movimiento.html', {'form': form, 'cuenta': cuenta}) + +@login_required +def actualizar_movimiento(request, movimiento_id): + movimiento = get_object_or_404(Movimientos, id=movimiento_id) + cuenta = movimiento.cuentas # Obtener la cuenta asociada + saldo_anterior = movimiento.saldo # Guardar saldo anterior + tipo_movimiento_anterior = movimiento.tipo_movimiento_id # Guardar tipo anterior + + if request.method == "POST": + form = RegistroMovimientosForm(request.POST, instance=movimiento) + if form.is_valid(): + nuevo_movimiento = form.save(commit=False) + + # Restaurar saldo antes de actualizar (solo si cambió el tipo o saldo) + if tipo_movimiento_anterior == 1: # Ingreso anterior + cuenta.saldo -= saldo_anterior + elif tipo_movimiento_anterior == 2: # Egreso anterior + cuenta.saldo += saldo_anterior + + # Aplicar nuevo saldo + if nuevo_movimiento.tipo_movimiento_id == 1: # Nuevo ingreso + cuenta.saldo += nuevo_movimiento.saldo + elif nuevo_movimiento.tipo_movimiento_id == 2: # Nuevo egreso + cuenta.saldo -= nuevo_movimiento.saldo + + cuenta.save() # Guardar la cuenta con el saldo actualizado + nuevo_movimiento.save() # Guardar el movimiento + + return redirect('lista_movimientos', cuenta_id=cuenta.id) + else: + form = RegistroMovimientosForm(instance=movimiento) + + return render(request, 'movimiento.html', {'form': form, 'cuenta': cuenta, 'movimiento': movimiento}) + +@login_required +def lista_cuentas(request): + if request.user.is_superuser: + cuentas = Cuenta.objects.all() # Si es superusuario, puede ver todas las cuentas + mostrar_todas = request.GET.get('todas', False) # Verifica si quiere ver todas las cuentas + if not mostrar_todas: + cuentas = cuentas.filter(responsable=request.user) # Muestra solo las propias si no activa la opción + else: + cuentas = Cuenta.objects.filter(responsable=request.user) # Usuario normal solo ve sus cuentas + + return render(request, 'lista_cuentas.html', {'cuentas': cuentas, 'es_superusuario': request.user.is_superuser}) + +@login_required +def lista_movimientos(request, cuenta_id): + cuenta = get_object_or_404(Cuenta, id=cuenta_id) # Obtiene la cuenta o muestra 404 + movimientos = Movimientos.objects.filter(cuentas=cuenta) # Filtra por cuenta + + return render(request, 'lista_movimientos.html', { + 'movimientos': movimientos, + 'cuenta': cuenta # Para mostrar el nombre en el template + }) + +@login_required +def exportar_movimientos_excel(request, cuenta_id): + # Obtener la cuenta + cuenta = get_object_or_404(Cuenta, id=cuenta_id) + + # Obtener los movimientos de la cuenta + movimientos = Movimientos.objects.filter(cuentas=cuenta) + + # Crear el archivo de Excel + wb = openpyxl.Workbook() + ws = wb.active + ws.title = f"Movimientos de {cuenta.cuenta}" + + # Establecer el nombre de la cuenta en la primera fila + ws.append([f"Cuenta: {cuenta.cuenta}"]) + ws.append([]) # Espacio vacío antes de la tabla + + # Agregar encabezados + encabezados = ["Fecha Inserción", "Tipo de Movimiento", "N° Factura", "Fecha Factura", "Proveedor", "Responsable", "Responsable Cuenta", "Saldo"] + ws.append(encabezados) + + # Agregar datos de los movimientos + for mov in movimientos: + ws.append([ + mov.fecha_insersion.strftime('%Y-%m-%d'), + mov.tipo_movimiento.tipo_movimiento, # Asegúrate de que el modelo tenga 'nombre' + mov.n_factura, + mov.fecha_factura.strftime('%Y-%m-%d') if mov.fecha_factura else '', + mov.proveedor, + mov.responsable.username, + mov.responsable_cuenta, + mov.saldo + ]) + + # Crear la respuesta HTTP con el archivo Excel + response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + response['Content-Disposition'] = f'attachment; filename="Movimientos_{cuenta.cuenta}.xlsx"' + wb.save(response) + + return response \ No newline at end of file