Referencia: Notificación de metas del mes {month} año {year}
-
Cordial saludo,
-
Mediante el presente comunicado nos permitimos informarle que, de acuerdo con el objeto de su contrato, la meta esperada para el mes de la referencia es la siguiente:
Referencia: Notificación de metas del mes {month} año {year}
+
Cordial saludo,
+
Mediante el presente comunicado nos permitimos informarle que, de acuerdo con el objeto de su contrato, la meta esperada para el mes de la referencia es la siguiente:
+
+
+
+
Descripción de la Variable a medir
+
Cantidad
+
+
+
{criteria}
+
+ {quantity}
+
+
+
+
+
Cordialmente,
+
+
+
+
+
+
___________________
+
Adriana Páez
+
Gerente de Operaciones
+
+
+
+
+
+
+
+ """
+
+ template = header + body
+ return template
diff --git a/INSIGHTSAPI/notifications/utils.py b/INSIGHTSAPI/notifications/utils.py
index 15b6b00d..52dfa8f5 100644
--- a/INSIGHTSAPI/notifications/utils.py
+++ b/INSIGHTSAPI/notifications/utils.py
@@ -1,32 +1,32 @@
-"""Utility functions for the notifications app."""
-
-from django.contrib.auth import get_user_model
-from django.core.mail import mail_admins
-
-from .models import Notification
-from .serializers import NotificationSerializer
-
-User = get_user_model()
-
-
-def create_notification(title: str, message: str, user) -> Notification | None:
- """Create a notification for a user."""
- if not isinstance(user, User):
- raise ValueError("user must be an instance of User model.")
- # Check the if the notification is valid
- notification_serializer = NotificationSerializer(
- data={"title": title, "message": message}
- )
- if not notification_serializer.is_valid():
- mail_admins(
- f"Error al crear la notificación {title}",
- f"La notificación para el usuario {user.get_full_name()} no es válida: {notification_serializer.errors}",
- )
- return None
- # Create the notification
- notification = Notification.objects.create(
- user=user,
- title=title,
- message=message,
- )
- return notification
+"""Utility functions for the notifications app."""
+
+from django.contrib.auth import get_user_model
+from django.core.mail import mail_admins
+
+from .models import Notification
+from .serializers import NotificationSerializer
+
+User = get_user_model()
+
+
+def create_notification(title: str, message: str, user) -> Notification | None:
+ """Create a notification for a user."""
+ if not isinstance(user, User):
+ raise ValueError("user must be an instance of User model.")
+ # Check the if the notification is valid
+ notification_serializer = NotificationSerializer(
+ data={"title": title, "message": message}
+ )
+ if not notification_serializer.is_valid():
+ mail_admins(
+ f"Error al crear la notificación {title}",
+ f"La notificación para el usuario {user.get_full_name()} no es válida: {notification_serializer.errors}",
+ )
+ return None
+ # Create the notification
+ notification = Notification.objects.create(
+ user=user,
+ title=title,
+ message=message,
+ )
+ return notification
diff --git a/INSIGHTSAPI/operational_risk/management/commands/upload_events.py b/INSIGHTSAPI/operational_risk/management/commands/upload_events.py
index 2364a712..60b17dfa 100644
--- a/INSIGHTSAPI/operational_risk/management/commands/upload_events.py
+++ b/INSIGHTSAPI/operational_risk/management/commands/upload_events.py
@@ -1,97 +1,97 @@
-import logging
-import os
-
-import pandas as pd
-from django.core.management.base import BaseCommand
-from ftfy import fix_text
-
-from operational_risk.models import (
- EventClass,
- Events,
- Level,
- LostType,
- Process,
- ProductLine,
-)
-
-logger = logging.getLogger("requests")
-
-
-class Command(BaseCommand):
- """Class to update the events from an Excel file"""
-
- help = "Update the events from an Excel file"
-
- def handle(self, *args, **options):
- """Method to handle the command"""
-
- # Load Excel file (update path as needed)
- file_path = os.path.join(os.getcwd(), "Eventos 2023.xlsx")
-
- # Read each sheet into a pandas DataFrame
- df = pd.read_excel(file_path, sheet_name=None)
-
- # Iterate over each row
- for event_row in df["Hoja1"].to_dict(orient="records"):
- # Fix text only for string values, skip others
- event_row = {
- k: fix_text(v) if isinstance(v, str) else v
- for k, v in event_row.items()
- }
-
- event_class = EventClass.objects.get(name=event_row["Clase de Evento"])
- level = None
- nivel = event_row["Nivel"].upper()
- if nivel == "BAJO":
- level, _ = Level.objects.get_or_create(name="BAJO")
- elif nivel == "MEDIO":
- level, _ = Level.objects.get_or_create(name="MEDIO")
- elif nivel == "ALTO":
- level, _ = Level.objects.get_or_create(name="ALTO")
-
- process = Process.objects.get(name=event_row["Proceso"])
- product = ProductLine.objects.get(name=event_row["Producto"])
-
- dates = [
- "Fecha de Inicio",
- "Fecha de Fin",
- "Fecha de Descubrimiento",
- "Fecha de Atención",
- "Fecha de Cierre",
- ]
- for date in dates:
- if event_row[date] == "0000-00-00":
- event_row[date] = None
- else:
- event_row[date] = pd.to_datetime(event_row[date], dayfirst=True)
-
- event_row["critico"] = bool(event_row["Clasificación"] == "CRITICO")
- event_row["estado"] = event_row["Estado Actual"] != "CERRADO"
-
- Events.objects.create(
- start_date=event_row["Fecha de Inicio"],
- end_date=event_row["Fecha de Fin"],
- discovery_date=event_row["Fecha de Descubrimiento"],
- accounting_date=event_row["Fecha de Atención"],
- currency=event_row["Divisa"],
- quantity=event_row["Cuantía"],
- recovered_quantity=event_row["Cuantía Total Recuperada"],
- recovered_quantity_by_insurance=event_row["Cuantía Rec. x Seguros"],
- event_class=event_class,
- reported_by=event_row["Reportado Por"],
- critical=event_row["critico"],
- level=level,
- plan=event_row["Plan"],
- event_title=event_row["Evento"],
- public_accounts_affected=event_row["Cuentas PUC Afectadas"],
- process=process,
- lost_type=LostType.objects.get(name=event_row["Tipo de Perdida"]),
- description=event_row["Descripción del Evento"],
- product=product,
- close_date=event_row["Fecha de Cierre"],
- learning=event_row["Aprendizaje"],
- status=event_row["estado"],
- )
- print(f"Event {event_row['Evento']} updated")
-
- self.stdout.write(self.style.SUCCESS("Events updated"))
+import logging
+import os
+
+import pandas as pd
+from django.core.management.base import BaseCommand
+from ftfy import fix_text
+
+from operational_risk.models import (
+ EventClass,
+ Events,
+ Level,
+ LostType,
+ Process,
+ ProductLine,
+)
+
+logger = logging.getLogger("requests")
+
+
+class Command(BaseCommand):
+ """Class to update the events from an Excel file"""
+
+ help = "Update the events from an Excel file"
+
+ def handle(self, *args, **options):
+ """Method to handle the command"""
+
+ # Load Excel file (update path as needed)
+ file_path = os.path.join(os.getcwd(), "Eventos 2023.xlsx")
+
+ # Read each sheet into a pandas DataFrame
+ df = pd.read_excel(file_path, sheet_name=None)
+
+ # Iterate over each row
+ for event_row in df["Hoja1"].to_dict(orient="records"):
+ # Fix text only for string values, skip others
+ event_row = {
+ k: fix_text(v) if isinstance(v, str) else v
+ for k, v in event_row.items()
+ }
+
+ event_class = EventClass.objects.get(name=event_row["Clase de Evento"])
+ level = None
+ nivel = event_row["Nivel"].upper()
+ if nivel == "BAJO":
+ level, _ = Level.objects.get_or_create(name="BAJO")
+ elif nivel == "MEDIO":
+ level, _ = Level.objects.get_or_create(name="MEDIO")
+ elif nivel == "ALTO":
+ level, _ = Level.objects.get_or_create(name="ALTO")
+
+ process = Process.objects.get(name=event_row["Proceso"])
+ product = ProductLine.objects.get(name=event_row["Producto"])
+
+ dates = [
+ "Fecha de Inicio",
+ "Fecha de Fin",
+ "Fecha de Descubrimiento",
+ "Fecha de Atención",
+ "Fecha de Cierre",
+ ]
+ for date in dates:
+ if event_row[date] == "0000-00-00":
+ event_row[date] = None
+ else:
+ event_row[date] = pd.to_datetime(event_row[date], dayfirst=True)
+
+ event_row["critico"] = bool(event_row["Clasificación"] == "CRITICO")
+ event_row["estado"] = event_row["Estado Actual"] != "CERRADO"
+
+ Events.objects.create(
+ start_date=event_row["Fecha de Inicio"],
+ end_date=event_row["Fecha de Fin"],
+ discovery_date=event_row["Fecha de Descubrimiento"],
+ accounting_date=event_row["Fecha de Atención"],
+ currency=event_row["Divisa"],
+ quantity=event_row["Cuantía"],
+ recovered_quantity=event_row["Cuantía Total Recuperada"],
+ recovered_quantity_by_insurance=event_row["Cuantía Rec. x Seguros"],
+ event_class=event_class,
+ reported_by=event_row["Reportado Por"],
+ critical=event_row["critico"],
+ level=level,
+ plan=event_row["Plan"],
+ event_title=event_row["Evento"],
+ public_accounts_affected=event_row["Cuentas PUC Afectadas"],
+ process=process,
+ lost_type=LostType.objects.get(name=event_row["Tipo de Perdida"]),
+ description=event_row["Descripción del Evento"],
+ product=product,
+ close_date=event_row["Fecha de Cierre"],
+ learning=event_row["Aprendizaje"],
+ status=event_row["estado"],
+ )
+ print(f"Event {event_row['Evento']} updated")
+
+ self.stdout.write(self.style.SUCCESS("Events updated"))
diff --git a/INSIGHTSAPI/payslip/serializers.py b/INSIGHTSAPI/payslip/serializers.py
index 26bf9401..122b2543 100644
--- a/INSIGHTSAPI/payslip/serializers.py
+++ b/INSIGHTSAPI/payslip/serializers.py
@@ -1,15 +1,15 @@
-"""Serializers for the payslip model. """
-
-from rest_framework import serializers
-from .models import Payslip
-
-
-class PayslipSerializer(serializers.ModelSerializer):
- """Serializer for the payslip model."""
-
- class Meta:
- """Meta class for the serializer."""
-
- model = Payslip
- fields = "__all__"
- read_only_fields = ("id", "created_at")
+"""Serializers for the payslip model. """
+
+from rest_framework import serializers
+from .models import Payslip
+
+
+class PayslipSerializer(serializers.ModelSerializer):
+ """Serializer for the payslip model."""
+
+ class Meta:
+ """Meta class for the serializer."""
+
+ model = Payslip
+ fields = "__all__"
+ read_only_fields = ("id", "created_at")
diff --git a/INSIGHTSAPI/pqrs/admin.py b/INSIGHTSAPI/pqrs/admin.py
index 4a8c8f5c..437dbfd7 100644
--- a/INSIGHTSAPI/pqrs/admin.py
+++ b/INSIGHTSAPI/pqrs/admin.py
@@ -1,11 +1,11 @@
-from django.contrib import admin
-
-from .models import PQRS
-
-
-@admin.register(PQRS)
-class PQRSAdmin(admin.ModelAdmin):
- list_display = ("reason", "user", "management", "created_at")
- search_fields = ("reason", "user", "management", "created_at")
- list_filter = ("reason", "management", "created_at")
- readonly_fields = ("created_at",)
+from django.contrib import admin
+
+from .models import PQRS
+
+
+@admin.register(PQRS)
+class PQRSAdmin(admin.ModelAdmin):
+ list_display = ("reason", "user", "management", "created_at")
+ search_fields = ("reason", "user", "management", "created_at")
+ list_filter = ("reason", "management", "created_at")
+ readonly_fields = ("created_at",)
diff --git a/INSIGHTSAPI/services/tests.py b/INSIGHTSAPI/services/tests.py
index 7642f5ab..aa800a75 100644
--- a/INSIGHTSAPI/services/tests.py
+++ b/INSIGHTSAPI/services/tests.py
@@ -1,200 +1,200 @@
-"""Test for services. """
-
-import os
-from datetime import timedelta
-from typing import Optional
-
-import holidays
-import requests
-from django.conf import settings
-from django.core.cache import cache
-from django.test import Client, TestCase, override_settings
-from django.urls import reverse
-from rest_framework.test import APITestCase
-
-from hierarchy.models import Area, JobPosition
-from services.models import Answer
-from users.models import User
-
-
-class BaseTestCase(APITestCase):
- """Base test case for all test cases."""
-
- databases = set(["default", "staffnet"])
-
- def logout(self):
- """Logout the user."""
- self.client.post(reverse("destroy-token"), {}, cookies=self.client.cookies) # type: ignore
-
- def setUp(self):
- """Set up the test case."""
- self.client.post(
- reverse("obtain-token"),
- {"username": "staffnet", "password": os.environ["StaffNetLDAP"]},
- )
- self.user = User.objects.get(username="staffnet")
-
- def create_demo_user(self, cedula: Optional[int] = None):
- """Create a demo user for tests."""
- demo_user = User.objects.create(
- cedula=cedula,
- username="test_user_" + str(User.objects.count() + 1),
- email=settings.EMAIL_FOR_TEST,
- first_name="Demo",
- last_name="User",
- )
- # Return the user object not the tuple
- if isinstance(demo_user, tuple):
- return demo_user[0]
- return demo_user
-
- def create_demo_user_admin(self):
- """Create a demo user with admin permissions."""
- # Set the id and
- demo_user = User.objects.get_or_create(
- pk=999,
- username="demo_admin",
- email=settings.EMAIL_FOR_TEST,
- first_name="Admin Demo",
- last_name="User",
- area=Area.objects.get_or_create(name="Admin")[0],
- job_position=JobPosition.objects.get_or_create(name="Admin", rank=100)[0],
- is_staff=True,
- is_superuser=True,
- )
- # Return the user object not the tuple
- if isinstance(demo_user, tuple):
- return demo_user[0]
- return demo_user
-
- # def create_demo_user_staffnet(self):
- # """Create a demo user with staffnet permissions."""
- # demo_user = User.objects.get_or_create(
- # cedula="1001185389",
- # username="demo_staffnet",
- # email=settings.EMAIL_FOR_TEST,
- # first_name="Staffnet Demo",
- # last_name="User",
- # area=Area.objects.get_or_create(name="Staffnet")[0],
- # job_position=JobPosition.objects.get_or_create(name="Staffnet", rank=1)[0],
- # )
- # # Return the user object not the tuple
- # if isinstance(demo_user, tuple):
- # return demo_user[0]
- # return demo_user
-
- def tearDown(self):
- """Tear down the test case."""
- self.logout()
-
-
-class StaticFilesTest(TestCase):
- """Test for static files."""
-
- def setUp(self):
- self.client = Client()
-
- def test_external_image_hosted(self):
- """Test that the external image is hosted."""
- url = f"https://{settings.ALLOWED_HOSTS[0]}/static/images/Logo_cyc_text.png"
- response = requests.get(url, timeout=5)
- self.assertEqual(response.status_code, 200)
-
- def test_nonexistent_static_file(self):
- """Test that a nonexistent static file returns a 404"""
- url = f"https://{settings.ALLOWED_HOSTS[0]}/static/services/non_exist_file.png"
- response = requests.get(url, timeout=5)
- self.assertEqual(response.status_code, 404)
-
-
-class EthicalLineTest(APITestCase):
- """Test for ethical line."""
-
- @override_settings(
- EMAIL_BACKEND="INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend"
- )
- def test_send_report_ethical_line_without_contact(self):
- """Test send report ethical line."""
- response = self.client.post(
- "/services/send-ethical-line/",
- {
- "complaint": "Test Type Without contact",
- "description": "Test Description",
- },
- )
- self.assertEqual(response.status_code, 200, response.data)
-
- @override_settings(
- EMAIL_BACKEND="INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend"
- )
- def test_send_report_ethical_line_with_contact(self):
- """Test send report ethical line."""
- response = self.client.post(
- "/services/send-ethical-line/",
- {
- "complaint": "Test Type with contact",
- "description": "Test Description",
- "contact_info": "Test contact info",
- },
- )
- self.assertEqual(response.status_code, 200, response.data)
-
-
-class HolidayTest(TestCase):
- """Test for holidays."""
-
- def setUp(self):
- cache.clear()
-
- def test_holiday(self):
- """Test that the holiday is a holiday."""
- self.assertTrue(holidays.Colombia().get("2022-01-01"))
-
- def test_non_holiday(self):
- """Test that the day is not a holiday."""
- self.assertFalse(holidays.Colombia().get("2022-01-02"))
-
- def test_get_holidays(self):
- """Test that the holidays are retrieved."""
- response = self.client.get("/services/holidays/2024/")
- self.assertEqual(response.status_code, 200, response.data)
- self.assertEqual(
- response.data, list(holidays.CO(years=range(2024, 2026)).items())
- )
-
-
-class QuestionTest(BaseTestCase):
- """Test for questions."""
-
- def test_save_answer(self):
- """Test save answer."""
- response = self.client.post(
- reverse("save_answer"),
- {
- "question_1": "Test Question 1",
- "question_2": "Test Question 2",
- "question_3": "Test Question 3",
- "duration": 1000000,
- },
- )
- self.assertEqual(response.status_code, 201, response.data)
- self.assertEqual(Answer.objects.count(), 1)
-
- def test_check_answered(self):
- """Test check answered."""
- response = self.client.get(reverse("check_answered"))
- self.assertEqual(response.status_code, 200, response.data)
- self.assertEqual(response.data, {"answered": False})
-
- def test_check_answered_true(self):
- """Test check answered."""
- Answer.objects.create(
- user=self.user,
- question_1="Test Question 1",
- question_2="Test Question 2",
- question_3="Test Question 3",
- duration=timedelta(microseconds=1000000),
- )
- response = self.client.get(reverse("check_answered"))
- self.assertEqual(response.status_code, 200, response.data)
- self.assertEqual(response.data, {"answered": True})
+"""Test for services. """
+
+import os
+from datetime import timedelta
+from typing import Optional
+
+import holidays
+import requests
+from django.conf import settings
+from django.core.cache import cache
+from django.test import Client, TestCase, override_settings
+from django.urls import reverse
+from rest_framework.test import APITestCase
+
+from hierarchy.models import Area, JobPosition
+from services.models import Answer
+from users.models import User
+
+
+class BaseTestCase(APITestCase):
+ """Base test case for all test cases."""
+
+ databases = set(["default", "staffnet"])
+
+ def logout(self):
+ """Logout the user."""
+ self.client.post(reverse("destroy-token"), {}, cookies=self.client.cookies) # type: ignore
+
+ def setUp(self):
+ """Set up the test case."""
+ self.client.post(
+ reverse("obtain-token"),
+ {"username": "staffnet", "password": os.environ["StaffNetLDAP"]},
+ )
+ self.user = User.objects.get(username="staffnet")
+
+ def create_demo_user(self, cedula: Optional[int] = None):
+ """Create a demo user for tests."""
+ demo_user = User.objects.create(
+ cedula=cedula,
+ username="test_user_" + str(User.objects.count() + 1),
+ email=settings.EMAIL_FOR_TEST,
+ first_name="Demo",
+ last_name="User",
+ )
+ # Return the user object not the tuple
+ if isinstance(demo_user, tuple):
+ return demo_user[0]
+ return demo_user
+
+ def create_demo_user_admin(self):
+ """Create a demo user with admin permissions."""
+ # Set the id and
+ demo_user = User.objects.get_or_create(
+ pk=999,
+ username="demo_admin",
+ email=settings.EMAIL_FOR_TEST,
+ first_name="Admin Demo",
+ last_name="User",
+ area=Area.objects.get_or_create(name="Admin")[0],
+ job_position=JobPosition.objects.get_or_create(name="Admin", rank=100)[0],
+ is_staff=True,
+ is_superuser=True,
+ )
+ # Return the user object not the tuple
+ if isinstance(demo_user, tuple):
+ return demo_user[0]
+ return demo_user
+
+ # def create_demo_user_staffnet(self):
+ # """Create a demo user with staffnet permissions."""
+ # demo_user = User.objects.get_or_create(
+ # cedula="1001185389",
+ # username="demo_staffnet",
+ # email=settings.EMAIL_FOR_TEST,
+ # first_name="Staffnet Demo",
+ # last_name="User",
+ # area=Area.objects.get_or_create(name="Staffnet")[0],
+ # job_position=JobPosition.objects.get_or_create(name="Staffnet", rank=1)[0],
+ # )
+ # # Return the user object not the tuple
+ # if isinstance(demo_user, tuple):
+ # return demo_user[0]
+ # return demo_user
+
+ def tearDown(self):
+ """Tear down the test case."""
+ self.logout()
+
+
+class StaticFilesTest(TestCase):
+ """Test for static files."""
+
+ def setUp(self):
+ self.client = Client()
+
+ def test_external_image_hosted(self):
+ """Test that the external image is hosted."""
+ url = f"https://{settings.ALLOWED_HOSTS[0]}/static/images/Logo_cyc_text.png"
+ response = requests.get(url, timeout=5)
+ self.assertEqual(response.status_code, 200)
+
+ def test_nonexistent_static_file(self):
+ """Test that a nonexistent static file returns a 404"""
+ url = f"https://{settings.ALLOWED_HOSTS[0]}/static/services/non_exist_file.png"
+ response = requests.get(url, timeout=5)
+ self.assertEqual(response.status_code, 404)
+
+
+class EthicalLineTest(APITestCase):
+ """Test for ethical line."""
+
+ @override_settings(
+ EMAIL_BACKEND="INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend"
+ )
+ def test_send_report_ethical_line_without_contact(self):
+ """Test send report ethical line."""
+ response = self.client.post(
+ "/services/send-ethical-line/",
+ {
+ "complaint": "Test Type Without contact",
+ "description": "Test Description",
+ },
+ )
+ self.assertEqual(response.status_code, 200, response.data)
+
+ @override_settings(
+ EMAIL_BACKEND="INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend"
+ )
+ def test_send_report_ethical_line_with_contact(self):
+ """Test send report ethical line."""
+ response = self.client.post(
+ "/services/send-ethical-line/",
+ {
+ "complaint": "Test Type with contact",
+ "description": "Test Description",
+ "contact_info": "Test contact info",
+ },
+ )
+ self.assertEqual(response.status_code, 200, response.data)
+
+
+class HolidayTest(TestCase):
+ """Test for holidays."""
+
+ def setUp(self):
+ cache.clear()
+
+ def test_holiday(self):
+ """Test that the holiday is a holiday."""
+ self.assertTrue(holidays.Colombia().get("2022-01-01"))
+
+ def test_non_holiday(self):
+ """Test that the day is not a holiday."""
+ self.assertFalse(holidays.Colombia().get("2022-01-02"))
+
+ def test_get_holidays(self):
+ """Test that the holidays are retrieved."""
+ response = self.client.get("/services/holidays/2024/")
+ self.assertEqual(response.status_code, 200, response.data)
+ self.assertEqual(
+ response.data, list(holidays.CO(years=range(2024, 2026)).items())
+ )
+
+
+class QuestionTest(BaseTestCase):
+ """Test for questions."""
+
+ def test_save_answer(self):
+ """Test save answer."""
+ response = self.client.post(
+ reverse("save_answer"),
+ {
+ "question_1": "Test Question 1",
+ "question_2": "Test Question 2",
+ "question_3": "Test Question 3",
+ "duration": 1000000,
+ },
+ )
+ self.assertEqual(response.status_code, 201, response.data)
+ self.assertEqual(Answer.objects.count(), 1)
+
+ def test_check_answered(self):
+ """Test check answered."""
+ response = self.client.get(reverse("check_answered"))
+ self.assertEqual(response.status_code, 200, response.data)
+ self.assertEqual(response.data, {"answered": False})
+
+ def test_check_answered_true(self):
+ """Test check answered."""
+ Answer.objects.create(
+ user=self.user,
+ question_1="Test Question 1",
+ question_2="Test Question 2",
+ question_3="Test Question 3",
+ duration=timedelta(microseconds=1000000),
+ )
+ response = self.client.get(reverse("check_answered"))
+ self.assertEqual(response.status_code, 200, response.data)
+ self.assertEqual(response.data, {"answered": True})
diff --git a/INSIGHTSAPI/services/views.py b/INSIGHTSAPI/services/views.py
index 81a21c62..80236527 100644
--- a/INSIGHTSAPI/services/views.py
+++ b/INSIGHTSAPI/services/views.py
@@ -1,131 +1,131 @@
-"""Views for the services app."""
-
-import logging
-import os
-import holidays
-from rest_framework.response import Response
-from django.http import JsonResponse
-from django.views.decorators.cache import cache_page, cache_control
-from rest_framework.decorators import api_view, permission_classes
-from rest_framework.permissions import AllowAny
-from rest_framework.views import APIView
-from django_sendfile import sendfile
-from services.models import Answer
-from datetime import timedelta
-from django.shortcuts import get_object_or_404
-from django.conf import settings
-from django.core.mail import send_mail
-
-
-logger = logging.getLogger("requests")
-
-CACHE_DURATION = 60 * 60 * 24 * 30 # 30 days
-
-
-class FileDownloadMixin(APIView):
- """Mixin for download files."""
-
- # The model have to be put in the views
- model = None
-
- def get(self, request, pk):
- """Get the file."""
- file_instance = get_object_or_404(self.model, pk=pk)
-
- file_path = file_instance.file.path
- file_name = file_name = os.path.basename(file_path)
-
- response = sendfile(
- request,
- file_path,
- attachment=True,
- attachment_filename=file_name,
- )
- return response
-
-
-@api_view(["POST"])
-@permission_classes([AllowAny])
-def send_report_ethical_line(request):
- """Send a report from the ethical line."""
- if not "complaint" in request.data:
- return Response({"error": "El tipo de denuncia es requerido"}, status=400)
- if not "description" in request.data:
- return Response(
- {"error": "La descripción de la denuncia es requerida"}, status=400
- )
-
- contact_info = ""
- if "contact_info" in request.data:
- contact_info = f"\nEl usuario desea ser contactado mediante:\n{request.data['contact_info']}"
- if settings.DEBUG or "test" in request.data["complaint"].lower():
- to_emails = [settings.EMAIL_FOR_TEST]
- else:
- to_emails = settings.EMAILS_ETHICAL_LINE
- send_mail(
- f"Denuncia de {request.data['complaint']}",
- f"\n{request.data['description']}\n" + contact_info,
- None,
- to_emails,
- fail_silently=False,
- html_message="True",
- )
-
- return Response({"message": "Correo enviado correctamente"}, status=200)
-
-
-def trigger_error(request):
- """Trigger an error for testing purposes."""
- raise Exception("Test error")
-
-
-@api_view(["GET"])
-@permission_classes([AllowAny])
-@cache_page(60 * 60 * 24, key_prefix="holidays")
-def get_holidays(request, year):
- """Get the holidays of the year."""
- try:
- year = int(year)
- except ValueError:
- return Response({"error": "El año debe ser un número"}, status=400)
- # Get the holidays of the year and the next year
- holidays_year = list(holidays.CO(years=range(year, year + 2)).items())
- return Response(holidays_year, status=200)
-
-
-@api_view(["POST"])
-def save_answer(request):
- """Save an answer."""
-
- if not "duration" in request.data:
- return Response({"error": "La duración es requerida"}, status=400)
- if not "question_1" in request.data:
- return Response({"error": "La pregunta 1 es requerida"}, status=400)
- if not "question_2" in request.data:
- return Response({"error": "La pregunta 2 es requerida"}, status=400)
- if not "question_3" in request.data:
- return Response({"error": "La pregunta 3 es requerida"}, status=400)
-
- try:
- duration = timedelta(microseconds=int(request.data["duration"]))
- except ValueError:
- return Response({"error": "La duración debe ser un número"}, status=400)
-
- answer = Answer(
- user=request.user,
- question_1=request.data["question_1"],
- question_2=request.data["question_2"],
- question_3=request.data["question_3"],
- duration=duration,
- )
- answer.save()
-
- return Response({"message": "Respuesta guardada correctamente"}, status=201)
-
-
-@api_view(["GET"])
-def check_answered(request):
- """Check if the user has answered the questions."""
- if Answer.objects.filter(user=request.user).exists():
- return Response({"answered": True}, status=200)
- return Response({"answered": False}, status=200)
+"""Views for the services app."""
+
+import logging
+import os
+import holidays
+from rest_framework.response import Response
+from django.http import JsonResponse
+from django.views.decorators.cache import cache_page, cache_control
+from rest_framework.decorators import api_view, permission_classes
+from rest_framework.permissions import AllowAny
+from rest_framework.views import APIView
+from django_sendfile import sendfile
+from services.models import Answer
+from datetime import timedelta
+from django.shortcuts import get_object_or_404
+from django.conf import settings
+from django.core.mail import send_mail
+
+
+logger = logging.getLogger("requests")
+
+CACHE_DURATION = 60 * 60 * 24 * 30 # 30 days
+
+
+class FileDownloadMixin(APIView):
+ """Mixin for download files."""
+
+ # The model have to be put in the views
+ model = None
+
+ def get(self, request, pk):
+ """Get the file."""
+ file_instance = get_object_or_404(self.model, pk=pk)
+
+ file_path = file_instance.file.path
+ file_name = file_name = os.path.basename(file_path)
+
+ response = sendfile(
+ request,
+ file_path,
+ attachment=True,
+ attachment_filename=file_name,
+ )
+ return response
+
+
+@api_view(["POST"])
+@permission_classes([AllowAny])
+def send_report_ethical_line(request):
+ """Send a report from the ethical line."""
+ if not "complaint" in request.data:
+ return Response({"error": "El tipo de denuncia es requerido"}, status=400)
+ if not "description" in request.data:
+ return Response(
+ {"error": "La descripción de la denuncia es requerida"}, status=400
+ )
+
+ contact_info = ""
+ if "contact_info" in request.data:
+ contact_info = f"\nEl usuario desea ser contactado mediante:\n{request.data['contact_info']}"
+ if settings.DEBUG or "test" in request.data["complaint"].lower():
+ to_emails = [settings.EMAIL_FOR_TEST]
+ else:
+ to_emails = settings.EMAILS_ETHICAL_LINE
+ send_mail(
+ f"Denuncia de {request.data['complaint']}",
+ f"\n{request.data['description']}\n" + contact_info,
+ None,
+ to_emails,
+ fail_silently=False,
+ html_message="True",
+ )
+
+ return Response({"message": "Correo enviado correctamente"}, status=200)
+
+
+def trigger_error(request):
+ """Trigger an error for testing purposes."""
+ raise Exception("Test error")
+
+
+@api_view(["GET"])
+@permission_classes([AllowAny])
+@cache_page(60 * 60 * 24, key_prefix="holidays")
+def get_holidays(request, year):
+ """Get the holidays of the year."""
+ try:
+ year = int(year)
+ except ValueError:
+ return Response({"error": "El año debe ser un número"}, status=400)
+ # Get the holidays of the year and the next year
+ holidays_year = list(holidays.CO(years=range(year, year + 2)).items())
+ return Response(holidays_year, status=200)
+
+
+@api_view(["POST"])
+def save_answer(request):
+ """Save an answer."""
+
+ if not "duration" in request.data:
+ return Response({"error": "La duración es requerida"}, status=400)
+ if not "question_1" in request.data:
+ return Response({"error": "La pregunta 1 es requerida"}, status=400)
+ if not "question_2" in request.data:
+ return Response({"error": "La pregunta 2 es requerida"}, status=400)
+ if not "question_3" in request.data:
+ return Response({"error": "La pregunta 3 es requerida"}, status=400)
+
+ try:
+ duration = timedelta(microseconds=int(request.data["duration"]))
+ except ValueError:
+ return Response({"error": "La duración debe ser un número"}, status=400)
+
+ answer = Answer(
+ user=request.user,
+ question_1=request.data["question_1"],
+ question_2=request.data["question_2"],
+ question_3=request.data["question_3"],
+ duration=duration,
+ )
+ answer.save()
+
+ return Response({"message": "Respuesta guardada correctamente"}, status=201)
+
+
+@api_view(["GET"])
+def check_answered(request):
+ """Check if the user has answered the questions."""
+ if Answer.objects.filter(user=request.user).exists():
+ return Response({"answered": True}, status=200)
+ return Response({"answered": False}, status=200)
diff --git a/INSIGHTSAPI/sgc/views.py b/INSIGHTSAPI/sgc/views.py
index 4c46557d..0960af7c 100644
--- a/INSIGHTSAPI/sgc/views.py
+++ b/INSIGHTSAPI/sgc/views.py
@@ -1,108 +1,108 @@
-"""Views for the SGC app"""
-
-import logging
-
-from rest_framework import viewsets
-from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
-
-from services.views import FileDownloadMixin
-
-from .models import SGCArea, SGCFile
-from .serializers import SGCAreaSerializer, SGCFileSerializer
-
-logger = logging.getLogger("requests")
-
-
-
-# ! This viewset is disabled until fix the cache issue
-
-# CACHE_DURATION = 60 * 15 # 15 minutes
-
-# class SGCFileViewSet(viewsets.ModelViewSet):
-# """ViewSet for the SGC class"""
-
-# queryset = SGCFile.objects.all().select_related("area")
-# serializer_class = SGCFileSerializer
-# permission_classes = [IsAuthenticated, DjangoModelPermissions]
-
-# @method_decorator(cache_page(CACHE_DURATION, key_prefix="sgc"))
-# def list(self, request, *args, **kwargs):
-# """List the objects"""
-# response = super().list(request, *args, **kwargs)
-# data_list = list(response.data)
-# permissions = {
-# "add": request.user.has_perm("sgc.add_sgcfile"),
-# "change": request.user.has_perm("sgc.change_sgcfile"),
-# "delete": request.user.has_perm("sgc.delete_sgcfile"),
-# }
-# response.data = {"objects": data_list, "permissions": permissions}
-# return response
-
-# def create(self, request, *args, **kwargs):
-# """Create a new object"""
-# response = super().create(request, *args, **kwargs)
-# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc"
-# return response
-
-# def update(self, request, *args, **kwargs):
-# """Update an object"""
-# response = super().update(request, *args, **kwargs)
-# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc"
-# return response
-
-# def destroy(self, request, *args, **kwargs):
-# """Destroy an object"""
-# response = super().destroy(request, *args, **kwargs)
-# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc"
-# return response
-
-
-class SGCFileViewSet(viewsets.ModelViewSet):
- """ViewSet for the SGC class"""
-
- queryset = SGCFile.objects.all().select_related("area")
- serializer_class = SGCFileSerializer
- permission_classes = [IsAuthenticated, DjangoModelPermissions]
-
- def list(self, request, *args, **kwargs):
- """List the objects"""
- response = super().list(request, *args, **kwargs)
- data_list = list(response.data)
- permissions = {
- "add": request.user.has_perm("sgc.add_sgcfile"),
- "change": request.user.has_perm("sgc.change_sgcfile"),
- "delete": request.user.has_perm("sgc.delete_sgcfile"),
- }
- response.data = {"objects": data_list, "permissions": permissions}
- return response
-
- def create(self, request, *args, **kwargs):
- """Create a new object"""
- response = super().create(request, *args, **kwargs)
- return response
-
- def update(self, request, *args, **kwargs):
- """Update an object"""
- response = super().update(request, *args, **kwargs)
- return response
-
- def destroy(self, request, *args, **kwargs):
- """Destroy an object"""
- response = super().destroy(request, *args, **kwargs)
- return response
-
-
-class SGCFileDownloadViewSet(FileDownloadMixin, viewsets.ReadOnlyModelViewSet):
- """ViewSet for the SGC class"""
-
- permission_classes = [IsAuthenticated, DjangoModelPermissions]
- model = SGCFile
- queryset = SGCFile.objects.all()
-
-
-class SGCAreaViewSet(viewsets.ReadOnlyModelViewSet):
- """ViewSet for the SGC class"""
-
- queryset = SGCArea.objects.all()
- serializer_class = SGCAreaSerializer
- permission_classes = [IsAuthenticated, DjangoModelPermissions]
+"""Views for the SGC app"""
+
+import logging
+
+from rest_framework import viewsets
+from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
+
+from services.views import FileDownloadMixin
+
+from .models import SGCArea, SGCFile
+from .serializers import SGCAreaSerializer, SGCFileSerializer
+
+logger = logging.getLogger("requests")
+
+
+
+# ! This viewset is disabled until fix the cache issue
+
+# CACHE_DURATION = 60 * 15 # 15 minutes
+
+# class SGCFileViewSet(viewsets.ModelViewSet):
+# """ViewSet for the SGC class"""
+
+# queryset = SGCFile.objects.all().select_related("area")
+# serializer_class = SGCFileSerializer
+# permission_classes = [IsAuthenticated, DjangoModelPermissions]
+
+# @method_decorator(cache_page(CACHE_DURATION, key_prefix="sgc"))
+# def list(self, request, *args, **kwargs):
+# """List the objects"""
+# response = super().list(request, *args, **kwargs)
+# data_list = list(response.data)
+# permissions = {
+# "add": request.user.has_perm("sgc.add_sgcfile"),
+# "change": request.user.has_perm("sgc.change_sgcfile"),
+# "delete": request.user.has_perm("sgc.delete_sgcfile"),
+# }
+# response.data = {"objects": data_list, "permissions": permissions}
+# return response
+
+# def create(self, request, *args, **kwargs):
+# """Create a new object"""
+# response = super().create(request, *args, **kwargs)
+# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc"
+# return response
+
+# def update(self, request, *args, **kwargs):
+# """Update an object"""
+# response = super().update(request, *args, **kwargs)
+# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc"
+# return response
+
+# def destroy(self, request, *args, **kwargs):
+# """Destroy an object"""
+# response = super().destroy(request, *args, **kwargs)
+# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc"
+# return response
+
+
+class SGCFileViewSet(viewsets.ModelViewSet):
+ """ViewSet for the SGC class"""
+
+ queryset = SGCFile.objects.all().select_related("area")
+ serializer_class = SGCFileSerializer
+ permission_classes = [IsAuthenticated, DjangoModelPermissions]
+
+ def list(self, request, *args, **kwargs):
+ """List the objects"""
+ response = super().list(request, *args, **kwargs)
+ data_list = list(response.data)
+ permissions = {
+ "add": request.user.has_perm("sgc.add_sgcfile"),
+ "change": request.user.has_perm("sgc.change_sgcfile"),
+ "delete": request.user.has_perm("sgc.delete_sgcfile"),
+ }
+ response.data = {"objects": data_list, "permissions": permissions}
+ return response
+
+ def create(self, request, *args, **kwargs):
+ """Create a new object"""
+ response = super().create(request, *args, **kwargs)
+ return response
+
+ def update(self, request, *args, **kwargs):
+ """Update an object"""
+ response = super().update(request, *args, **kwargs)
+ return response
+
+ def destroy(self, request, *args, **kwargs):
+ """Destroy an object"""
+ response = super().destroy(request, *args, **kwargs)
+ return response
+
+
+class SGCFileDownloadViewSet(FileDownloadMixin, viewsets.ReadOnlyModelViewSet):
+ """ViewSet for the SGC class"""
+
+ permission_classes = [IsAuthenticated, DjangoModelPermissions]
+ model = SGCFile
+ queryset = SGCFile.objects.all()
+
+
+class SGCAreaViewSet(viewsets.ReadOnlyModelViewSet):
+ """ViewSet for the SGC class"""
+
+ queryset = SGCArea.objects.all()
+ serializer_class = SGCAreaSerializer
+ permission_classes = [IsAuthenticated, DjangoModelPermissions]
diff --git a/INSIGHTSAPI/users/admin.py b/INSIGHTSAPI/users/admin.py
index b4a39822..43e4df3a 100644
--- a/INSIGHTSAPI/users/admin.py
+++ b/INSIGHTSAPI/users/admin.py
@@ -1,58 +1,58 @@
-from django.contrib import admin
-from django.contrib.auth.admin import UserAdmin
-from .models import User
-
-
-@admin.display(description="Name")
-def upper_case_name(obj):
- """Display the user's name in uppercase."""
- return obj.get_full_name()
-
-
-@admin.register(User)
-class CustomUserAdmin(UserAdmin):
- """Custom user admin."""
-
- readonly_fields = ["username"]
-
- list_display = (
- upper_case_name,
- "job_position",
- "area",
- )
- list_filter = (
- "is_staff",
- "is_superuser",
- "groups",
- )
- search_fields = (
- "username",
- "first_name",
- "last_name",
- )
- ordering = ("first_name", "area", "job_position")
- filter_horizontal = (
- "groups",
- "user_permissions",
- )
- fieldsets = (
- (
- "Información personal",
- {"fields": (("first_name", "last_name"), "email")},
- ),
- (
- "Información corporativa",
- {"fields": ("username", ("area", "company_email"), "job_position")},
- ),
- (
- "Permisos",
- {
- "fields": (
- "is_staff",
- "is_superuser",
- "groups",
- "user_permissions",
- ),
- },
- ),
- )
+from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin
+from .models import User
+
+
+@admin.display(description="Name")
+def upper_case_name(obj):
+ """Display the user's name in uppercase."""
+ return obj.get_full_name()
+
+
+@admin.register(User)
+class CustomUserAdmin(UserAdmin):
+ """Custom user admin."""
+
+ readonly_fields = ["username"]
+
+ list_display = (
+ upper_case_name,
+ "job_position",
+ "area",
+ )
+ list_filter = (
+ "is_staff",
+ "is_superuser",
+ "groups",
+ )
+ search_fields = (
+ "username",
+ "first_name",
+ "last_name",
+ )
+ ordering = ("first_name", "area", "job_position")
+ filter_horizontal = (
+ "groups",
+ "user_permissions",
+ )
+ fieldsets = (
+ (
+ "Información personal",
+ {"fields": (("first_name", "last_name"), "email")},
+ ),
+ (
+ "Información corporativa",
+ {"fields": ("username", ("area", "company_email"), "job_position")},
+ ),
+ (
+ "Permisos",
+ {
+ "fields": (
+ "is_staff",
+ "is_superuser",
+ "groups",
+ "user_permissions",
+ ),
+ },
+ ),
+ )
diff --git a/INSIGHTSAPI/users/factories.py b/INSIGHTSAPI/users/factories.py
index 84a7adfa..06c57b61 100644
--- a/INSIGHTSAPI/users/factories.py
+++ b/INSIGHTSAPI/users/factories.py
@@ -11,7 +11,7 @@ class UserFactory(DjangoModelFactory):
class Meta:
model = User
- factory.Faker._DEFAULT_LOCALE = "es_ES"
+ factory.Faker._DEFAULT_LOCALE = "es_CO"
cedula = factory.Faker("random_int", min=1000000000, max=9999999999)
first_name = factory.LazyFunction(lambda: "Fake " + fake_data.first_name())
last_name = factory.Faker("last_name")
diff --git a/INSIGHTSAPI/users/tests.py b/INSIGHTSAPI/users/tests.py
index bb244ab6..f9f40f85 100644
--- a/INSIGHTSAPI/users/tests.py
+++ b/INSIGHTSAPI/users/tests.py
@@ -1,374 +1,374 @@
-"""Tests for the users app."""
-
-import os
-import random
-
-import ldap # type: ignore
-from django.conf import settings
-from django.contrib.auth.models import Permission
-from django.core.files.uploadedfile import SimpleUploadedFile
-from django.test import TestCase
-from django.test.client import Client
-from django.urls import reverse
-
-from hierarchy.models import Area
-from notifications.models import Notification
-from services.tests import BaseTestCase
-from users.models import User
-
-
-class LDAPAuthenticationTest(TestCase):
- """Tests the LDAP authentication."""
-
- databases = "__all__"
-
- def setUp(self):
- """Sets up the test client."""
- self.client = Client()
-
- def test_ldap_connection(self):
- """Tests that the connection to the LDAP server is successful."""
- ldap_server_uri = settings.AUTH_LDAP_SERVER_URI
- ldap_bind_dn = settings.AUTH_LDAP_BIND_DN
- ldap_bind_password = settings.AUTH_LDAP_BIND_PASSWORD
- conn = None
- try:
- conn = ldap.initialize(ldap_server_uri)
- conn.simple_bind_s(ldap_bind_dn, ldap_bind_password)
- except ldap.LDAPError as err:
- self.fail(f"Error: {err}")
- finally:
- if conn:
- conn.unbind_s()
-
- def test_login(self):
- """Tests that the user can login using LDAP."""
- ldap_server_uri = settings.AUTH_LDAP_SERVER_URI
- ldap_bind_dn = settings.AUTH_LDAP_BIND_DN
- ldap_bind_password = settings.AUTH_LDAP_BIND_PASSWORD
- username = "staffnet"
- password = os.environ["StaffNetLDAP"]
- conn = None
- try:
- conn = ldap.initialize(ldap_server_uri)
- conn.simple_bind_s(ldap_bind_dn, ldap_bind_password)
- search_filter = "(sAMAccountName={})".format(username)
- search_base = "dc=CYC-SERVICES,dc=COM,dc=CO"
- attributes = ["dn"]
- result_id = conn.search(
- search_base, ldap.SCOPE_SUBTREE, search_filter, attributes
- )
- _, result_data = conn.result(result_id, 0)
- self.assertTrue(result_data, "User entry not found.")
- if result_data:
- user_dn = result_data[0][0]
- logged = conn.simple_bind_s(user_dn, password)
- self.assertTrue(logged, "User authentication failed.")
- except ldap.LDAPError as err:
- self.fail("Error: %s" % err)
- finally:
- if conn:
- conn.unbind()
-
- def test_login_django(self, called=False):
- """Tests that the login endpoint works as expected."""
- if called:
- username = "staffnet"
- password = os.environ["StaffNetLDAP"]
- data = {
- "username": username,
- "password": password,
- }
- response = self.client.post(reverse("obtain-token"), data)
- self.assertEqual(response.status_code, 200, response.data)
- token = self.client.cookies.get("access-token")
- refresh = self.client.cookies.get("refresh-token")
- self.assertIsNotNone(token, "No authentication token found in the response")
- self.assertIsNotNone(refresh, "No refresh token found in the response")
- self.assertEqual(User.objects.count(), 1)
- user = User.objects.first()
- if user:
- self.assertEqual(str(user.username).lower(), username)
- self.assertEqual(user.email, settings.EMAIL_FOR_TEST)
- self.assertEqual(user.first_name, "STAFFNET")
- self.assertEqual(user.last_name, "LDAP")
- self.assertEqual(user.job_position.name, "Administrador")
- self.assertEqual(user.job_position.rank, 9)
- self.assertEqual(user.area.name, "Administrador")
- self.assertEqual(user.area.manager, user)
- self.assertIsNotNone(user.last_login)
- else:
- self.fail("User not created.")
- return response
-
- def test_login_update_user_without_windows_user(self):
- """Tests that the login endpoint fails when the user is not in the windows server."""
- username = "staffnet"
- password = os.environ["StaffNetLDAP"]
- data = {
- "username": username,
- "password": password,
- }
- User.objects.create(
- username="",
- cedula="00000000",
- first_name="Administrador",
- last_name="",
- )
- response = self.client.post(reverse("obtain-token"), data)
- self.assertEqual(response.status_code, 200, response.data)
-
- def test_login_fail(self):
- """Tests that the login endpoint fails when the password is wrong."""
- username = "staffnet"
- password = "WrongPassword"
- data = {
- "username": username,
- "password": password,
- }
- response = self.client.post(reverse("obtain-token"), data)
- self.assertEqual(response.status_code, 401)
- token = self.client.cookies.get("access-token")
- self.assertIsNone(token, "Authentication token found in the response")
-
- def test_logout(self):
- """Tests that the logout endpoint works as expected."""
- response = self.test_login_django(called=True)
- # Make a request that requires authentication
- response = self.client.get("/contracts/", cookies=self.client.cookies) # type: ignore
- self.assertEqual(response.status_code, 403)
- response2 = self.client.post(reverse("destroy-token"), cookies=self.client.cookies) # type: ignore
- self.assertEqual(response2.status_code, 200)
- response = self.client.get("/goals/", cookies=self.client.cookies) # type: ignore
- self.assertEqual(response.status_code, 401)
-
-
-class UserTestCase(BaseTestCase):
-
- def setUp(self):
- """Sets up the test client."""
- super().setUp()
- self.user.user_permissions.add(Permission.objects.get(codename="upload_points"))
-
- def test_get_full_name(self):
- """Tests that the full name is returned correctly."""
- user = User(first_name="David", last_name="Alvarez")
- self.assertEqual(user.get_full_name(), "David Alvarez")
-
- def test_get_full_name_reversed(self):
- """Tests that the full name is returned correctly."""
- user = User(first_name="David", last_name="Alvarez")
- self.assertEqual(user.get_full_name_reversed(), "Alvarez David")
-
- def test_get_subordinates_same_area(self):
- """Tests that the get_users endpoint can return users from the same area."""
- self.user.job_position.rank = 3
- self.user.job_position.save()
- demo_user = self.create_demo_user()
- demo_user.area = self.user.area
- demo_user.save()
- response = self.client.get(reverse("get_subordinates"))
- self.assertEqual(response.status_code, 200)
- self.assertEqual(
- response.data,
- [
- {"id": demo_user.pk, "name": demo_user.get_full_name()},
- ],
- )
-
- def test_get_subordinates_multiple_area(self):
- """Tests that the get_users endpoint can return users from multiple areas if the user is the manager."""
- self.user.job_position.rank = 3
- self.user.job_position.save()
- demo_user_1 = self.create_demo_user()
- demo_user_1.area = Area.objects.get_or_create(name="Demo")[0]
- demo_user_1.area.manager = self.user
- demo_user_1.area.save()
- demo_user_1.save()
- demo_user_2 = self.create_demo_user()
- demo_user_2.area = Area.objects.get_or_create(name="Demo")[0]
- demo_user_2.save()
- response = self.client.get(reverse("get_subordinates"))
- self.assertEqual(response.status_code, 200)
- self.assertEqual(
- response.data,
- [
- {"id": demo_user_1.pk, "name": demo_user_1.get_full_name()},
- {"id": demo_user_2.pk, "name": demo_user_2.get_full_name()},
- ],
- )
-
- def test_get_subordinates_area_no_manager(self):
- """Tests that the get_users endpoint does not return users from areas that the user does not manage."""
- demo_user = self.create_demo_user()
- demo_user.area = Area.objects.get_or_create(name="Demo")[0]
- demo_user.save()
- response = self.client.get(reverse("get_subordinates"))
- self.assertEqual(response.status_code, 200)
- # response.data should be 1 that is the user itself
- self.assertEqual(len(response.data), 1)
-
- def test_get_subordinates_higher_rank(self):
- """Tests that the get_users endpoint does not return users with a higher rank."""
- boss = self.create_demo_user()
- boss.job_position.rank = 100
- boss.job_position.save()
- response = self.client.get(reverse("get_subordinates"))
- self.assertEqual(response.status_code, 200)
- # response.data should be 1 that is the user itself
- self.assertEqual(len(response.data), 1)
-
- def test_get_subordinates_manager(self):
- """Tests that the get_users endpoint returns the manager of the user."""
- self.user.job_position.rank = 4
- response = self.client.get(reverse("get_subordinates"))
- self.assertEqual(response.status_code, 200)
- self.assertEqual(
- response.data, [{"id": self.user.pk, "name": self.user.get_full_name()}]
- )
-
- def test_get_subordinates_no_manager(self):
- """Tests that the get_users endpoint returns an empty list if the user does not have a manager."""
- self.user.job_position.rank = 1
- self.user.job_position.save()
- response = self.client.get(reverse("get_subordinates"))
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.data, [])
-
- def test_get_user_profile(self):
- """Tests that the get_user_profile endpoint works as expected."""
- self.user.cedula = settings.TEST_CEDULA
- self.user.save()
- response = self.client.get(reverse("get_profile"))
- self.assertEqual(response.status_code, 200, response.data)
-
- def test_update_user(self):
- """Tests that the update_user endpoint works as expected."""
- # Create a random number for cell phone
- data = {"estado_civil": "Soltero", "hijos": random.randint(0, 999999)}
- self.user.cedula = settings.TEST_CEDULA
- self.user.save()
- response = self.client.patch(reverse("update_profile"), data)
- self.assertEqual(response.status_code, 200, response.data)
-
- def test_update_user_email(self):
- """Tests that the update_user endpoint works as expected."""
- data = {"correo": "test{}@cyc-bpo.com".format(random.randint(0, 999999))}
- self.user.cedula = 1001185389
- self.user.save()
- response = self.client.patch(reverse("update_profile"), data)
- self.assertEqual(response.status_code, 200, response.data)
- self.user.refresh_from_db()
- self.assertEqual(self.user.email, str(data["correo"]).upper())
-
- def test_update_user_mail_invalid(self):
- """Tests that the update_user endpoint returns an error if the email is invalid."""
- data = {"correo": "test@invalid"}
- self.user.cedula = 1001185389
- self.user.save()
- response = self.client.patch(reverse("update_profile"), data)
- self.assertEqual(response.status_code, 400, response.data)
- self.assertEqual(
- response.data["error"],
- "El correo ingresado no es válido, por favor verifica e intenta de nuevo.",
- )
-
- def test_user_creation(self):
- """Tests that the user creation works as expected."""
- user = User.objects.create(
- username="user_test".format(random.randint(0, 999999)),
- cedula=os.environ["TEST_CEDULA"],
- first_name="Test",
- last_name="User",
- )
- self.assertEqual(User.objects.count(), 2)
- self.assertEqual(
- User.objects.get(pk=user.pk).company_email,
- os.environ["EMAIL_FOR_TEST"].upper(),
- )
-
- def test_get_points(self):
- """Tests that the get_points endpoint works as expected."""
- self.create_demo_user()
- self.user.points = 100
- self.user.save()
- response = self.client.get(reverse("get_points"))
- self.assertEqual(response.status_code, 200, response.data)
- self.assertEqual(len(response.data), len(User.objects.all()))
- self.assertEqual(response.data[0]["points"], 100)
-
- def test_get_points_unauthenticated(self):
- """Tests that the get_points endpoint returns an error if the user is not authenticated."""
- self.client.logout()
- response = self.client.get(reverse("get_points"))
- self.assertEqual(response.status_code, 401)
-
- def test_upload_points(self):
- """Tests that the upload_points endpoint works as expected."""
- self.create_demo_user(1001185386)
- self.create_demo_user(1001185390)
- content = "cedula;puntos\n1001185386;100\n1001185390;200"
- file = SimpleUploadedFile("points.csv", content.encode("utf-8"))
- response = self.client.post(reverse("upload_points"), {"file": file})
- self.assertEqual(response.status_code, 200, response.data)
- self.assertEqual(User.objects.get(cedula=1001185386).points, 100)
- self.assertEqual(User.objects.get(cedula=1001185390).points, 200)
-
- def test_upload_points_unauthenticated(self):
- """Tests that the upload_points endpoint returns an error if the user is not authenticated."""
- self.client.logout()
- response = self.client.post(reverse("upload_points"))
- self.assertEqual(response.status_code, 401)
-
- def test_upload_points_wrong_file(self):
- """Tests that the upload_points endpoint returns an error if the file is not a CSV."""
- file = SimpleUploadedFile("points.xlsx", b"")
- response = self.client.post(reverse("upload_points"), {"file": file})
- self.assertEqual(response.status_code, 400)
- self.assertEqual(
- response.data["error"],
- "El archivo debe ser un archivo CSV, por favor verifica e intenta de nuevo.",
- )
-
- def test_upload_points_wrong_columns(self):
- """Tests that the upload_points endpoint returns an error if the file does not have the correct columns."""
- content = "cedula;score\n1001185386;100\n1001185390;20"
- file = SimpleUploadedFile("points.csv", content.encode("utf-8"))
- response = self.client.post(reverse("upload_points"), {"file": file})
- self.assertEqual(response.status_code, 400)
- self.assertEqual(
- response.data["error"],
- "El archivo debe tener dos columnas llamadas 'cedula' y 'puntos', por favor verifica e intenta de nuevo.",
- )
-
- def test_upload_points_user_not_found(self):
- """Tests that the upload_points endpoint returns an error if the user is not found."""
- self.create_demo_user(1001185386)
- content = "cedula;puntos\n1001185386;100\n1001185391;200"
- file = SimpleUploadedFile("points.csv", content.encode("utf-8"))
- response = self.client.post(reverse("upload_points"), {"file": file})
- self.assertEqual(response.status_code, 400)
- self.assertEqual(
- response.data["error"],
- "Actualización exitosa, pero algunos usuarios no fueron encontrados: 1001185391",
- )
- self.assertEqual(Notification.objects.count(), 1)
-
- def test_upload_points_not_perm(self):
- """Tests that the upload_points endpoint returns an error if the user does not have the permission."""
- self.user.user_permissions.clear()
- content = "cedula;puntos,1001185386;100,1001185390;200"
- file = SimpleUploadedFile("points.csv", content.encode("utf-8"))
- response = self.client.post(reverse("upload_points"), {"file": file})
- self.assertEqual(response.status_code, 403)
-
- def test_upload_points_read_file(self):
- """Tests that the upload_points endpoint reads the file correctly."""
- self.create_demo_user(1001185386)
- self.create_demo_user(1001185390)
- with open("utils/excels/puntos-cyc.csv", "rb") as f:
- file = SimpleUploadedFile("points.csv", f.read())
- response = self.client.post(reverse("upload_points"), {"file": file})
- self.assertEqual(response.status_code, 200, response.data)
- self.assertEqual(User.objects.get(cedula=1001185386).points, 133)
- self.assertEqual(User.objects.get(cedula=1001185390).points, 20)
+"""Tests for the users app."""
+
+import os
+import random
+
+import ldap # type: ignore
+from django.conf import settings
+from django.contrib.auth.models import Permission
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.test import TestCase
+from django.test.client import Client
+from django.urls import reverse
+
+from hierarchy.models import Area
+from notifications.models import Notification
+from services.tests import BaseTestCase
+from users.models import User
+
+
+class LDAPAuthenticationTest(TestCase):
+ """Tests the LDAP authentication."""
+
+ databases = "__all__"
+
+ def setUp(self):
+ """Sets up the test client."""
+ self.client = Client()
+
+ def test_ldap_connection(self):
+ """Tests that the connection to the LDAP server is successful."""
+ ldap_server_uri = settings.AUTH_LDAP_SERVER_URI
+ ldap_bind_dn = settings.AUTH_LDAP_BIND_DN
+ ldap_bind_password = settings.AUTH_LDAP_BIND_PASSWORD
+ conn = None
+ try:
+ conn = ldap.initialize(ldap_server_uri)
+ conn.simple_bind_s(ldap_bind_dn, ldap_bind_password)
+ except ldap.LDAPError as err:
+ self.fail(f"Error: {err}")
+ finally:
+ if conn:
+ conn.unbind_s()
+
+ def test_login(self):
+ """Tests that the user can login using LDAP."""
+ ldap_server_uri = settings.AUTH_LDAP_SERVER_URI
+ ldap_bind_dn = settings.AUTH_LDAP_BIND_DN
+ ldap_bind_password = settings.AUTH_LDAP_BIND_PASSWORD
+ username = "staffnet"
+ password = os.environ["StaffNetLDAP"]
+ conn = None
+ try:
+ conn = ldap.initialize(ldap_server_uri)
+ conn.simple_bind_s(ldap_bind_dn, ldap_bind_password)
+ search_filter = "(sAMAccountName={})".format(username)
+ search_base = "dc=CYC-SERVICES,dc=COM,dc=CO"
+ attributes = ["dn"]
+ result_id = conn.search(
+ search_base, ldap.SCOPE_SUBTREE, search_filter, attributes
+ )
+ _, result_data = conn.result(result_id, 0)
+ self.assertTrue(result_data, "User entry not found.")
+ if result_data:
+ user_dn = result_data[0][0]
+ logged = conn.simple_bind_s(user_dn, password)
+ self.assertTrue(logged, "User authentication failed.")
+ except ldap.LDAPError as err:
+ self.fail("Error: %s" % err)
+ finally:
+ if conn:
+ conn.unbind()
+
+ def test_login_django(self, called=False):
+ """Tests that the login endpoint works as expected."""
+ if called:
+ username = "staffnet"
+ password = os.environ["StaffNetLDAP"]
+ data = {
+ "username": username,
+ "password": password,
+ }
+ response = self.client.post(reverse("obtain-token"), data)
+ self.assertEqual(response.status_code, 200, response.data)
+ token = self.client.cookies.get("access-token")
+ refresh = self.client.cookies.get("refresh-token")
+ self.assertIsNotNone(token, "No authentication token found in the response")
+ self.assertIsNotNone(refresh, "No refresh token found in the response")
+ self.assertEqual(User.objects.count(), 1)
+ user = User.objects.first()
+ if user:
+ self.assertEqual(str(user.username).lower(), username)
+ self.assertEqual(user.email, settings.EMAIL_FOR_TEST)
+ self.assertEqual(user.first_name, "STAFFNET")
+ self.assertEqual(user.last_name, "LDAP")
+ self.assertEqual(user.job_position.name, "Administrador")
+ self.assertEqual(user.job_position.rank, 9)
+ self.assertEqual(user.area.name, "Administrador")
+ self.assertEqual(user.area.manager, user)
+ self.assertIsNotNone(user.last_login)
+ else:
+ self.fail("User not created.")
+ return response
+
+ def test_login_update_user_without_windows_user(self):
+ """Tests that the login endpoint fails when the user is not in the windows server."""
+ username = "staffnet"
+ password = os.environ["StaffNetLDAP"]
+ data = {
+ "username": username,
+ "password": password,
+ }
+ User.objects.create(
+ username="",
+ cedula="00000000",
+ first_name="Administrador",
+ last_name="",
+ )
+ response = self.client.post(reverse("obtain-token"), data)
+ self.assertEqual(response.status_code, 200, response.data)
+
+ def test_login_fail(self):
+ """Tests that the login endpoint fails when the password is wrong."""
+ username = "staffnet"
+ password = "WrongPassword"
+ data = {
+ "username": username,
+ "password": password,
+ }
+ response = self.client.post(reverse("obtain-token"), data)
+ self.assertEqual(response.status_code, 401)
+ token = self.client.cookies.get("access-token")
+ self.assertIsNone(token, "Authentication token found in the response")
+
+ def test_logout(self):
+ """Tests that the logout endpoint works as expected."""
+ response = self.test_login_django(called=True)
+ # Make a request that requires authentication
+ response = self.client.get("/contracts/", cookies=self.client.cookies) # type: ignore
+ self.assertEqual(response.status_code, 403)
+ response2 = self.client.post(reverse("destroy-token"), cookies=self.client.cookies) # type: ignore
+ self.assertEqual(response2.status_code, 200)
+ response = self.client.get("/goals/", cookies=self.client.cookies) # type: ignore
+ self.assertEqual(response.status_code, 401)
+
+
+class UserTestCase(BaseTestCase):
+
+ def setUp(self):
+ """Sets up the test client."""
+ super().setUp()
+ self.user.user_permissions.add(Permission.objects.get(codename="upload_points"))
+
+ def test_get_full_name(self):
+ """Tests that the full name is returned correctly."""
+ user = User(first_name="David", last_name="Alvarez")
+ self.assertEqual(user.get_full_name(), "David Alvarez")
+
+ def test_get_full_name_reversed(self):
+ """Tests that the full name is returned correctly."""
+ user = User(first_name="David", last_name="Alvarez")
+ self.assertEqual(user.get_full_name_reversed(), "Alvarez David")
+
+ def test_get_subordinates_same_area(self):
+ """Tests that the get_users endpoint can return users from the same area."""
+ self.user.job_position.rank = 3
+ self.user.job_position.save()
+ demo_user = self.create_demo_user()
+ demo_user.area = self.user.area
+ demo_user.save()
+ response = self.client.get(reverse("get_subordinates"))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.data,
+ [
+ {"id": demo_user.pk, "name": demo_user.get_full_name()},
+ ],
+ )
+
+ def test_get_subordinates_multiple_area(self):
+ """Tests that the get_users endpoint can return users from multiple areas if the user is the manager."""
+ self.user.job_position.rank = 3
+ self.user.job_position.save()
+ demo_user_1 = self.create_demo_user()
+ demo_user_1.area = Area.objects.get_or_create(name="Demo")[0]
+ demo_user_1.area.manager = self.user
+ demo_user_1.area.save()
+ demo_user_1.save()
+ demo_user_2 = self.create_demo_user()
+ demo_user_2.area = Area.objects.get_or_create(name="Demo")[0]
+ demo_user_2.save()
+ response = self.client.get(reverse("get_subordinates"))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.data,
+ [
+ {"id": demo_user_1.pk, "name": demo_user_1.get_full_name()},
+ {"id": demo_user_2.pk, "name": demo_user_2.get_full_name()},
+ ],
+ )
+
+ def test_get_subordinates_area_no_manager(self):
+ """Tests that the get_users endpoint does not return users from areas that the user does not manage."""
+ demo_user = self.create_demo_user()
+ demo_user.area = Area.objects.get_or_create(name="Demo")[0]
+ demo_user.save()
+ response = self.client.get(reverse("get_subordinates"))
+ self.assertEqual(response.status_code, 200)
+ # response.data should be 1 that is the user itself
+ self.assertEqual(len(response.data), 1)
+
+ def test_get_subordinates_higher_rank(self):
+ """Tests that the get_users endpoint does not return users with a higher rank."""
+ boss = self.create_demo_user()
+ boss.job_position.rank = 100
+ boss.job_position.save()
+ response = self.client.get(reverse("get_subordinates"))
+ self.assertEqual(response.status_code, 200)
+ # response.data should be 1 that is the user itself
+ self.assertEqual(len(response.data), 1)
+
+ def test_get_subordinates_manager(self):
+ """Tests that the get_users endpoint returns the manager of the user."""
+ self.user.job_position.rank = 4
+ response = self.client.get(reverse("get_subordinates"))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.data, [{"id": self.user.pk, "name": self.user.get_full_name()}]
+ )
+
+ def test_get_subordinates_no_manager(self):
+ """Tests that the get_users endpoint returns an empty list if the user does not have a manager."""
+ self.user.job_position.rank = 1
+ self.user.job_position.save()
+ response = self.client.get(reverse("get_subordinates"))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data, [])
+
+ def test_get_user_profile(self):
+ """Tests that the get_user_profile endpoint works as expected."""
+ self.user.cedula = settings.TEST_CEDULA
+ self.user.save()
+ response = self.client.get(reverse("get_profile"))
+ self.assertEqual(response.status_code, 200, response.data)
+
+ def test_update_user(self):
+ """Tests that the update_user endpoint works as expected."""
+ # Create a random number for cell phone
+ data = {"estado_civil": "Soltero", "hijos": random.randint(0, 999999)}
+ self.user.cedula = settings.TEST_CEDULA
+ self.user.save()
+ response = self.client.patch(reverse("update_profile"), data)
+ self.assertEqual(response.status_code, 200, response.data)
+
+ def test_update_user_email(self):
+ """Tests that the update_user endpoint works as expected."""
+ data = {"correo": "test{}@cyc-bpo.com".format(random.randint(0, 999999))}
+ self.user.cedula = 1001185389
+ self.user.save()
+ response = self.client.patch(reverse("update_profile"), data)
+ self.assertEqual(response.status_code, 200, response.data)
+ self.user.refresh_from_db()
+ self.assertEqual(self.user.email, str(data["correo"]).upper())
+
+ def test_update_user_mail_invalid(self):
+ """Tests that the update_user endpoint returns an error if the email is invalid."""
+ data = {"correo": "test@invalid"}
+ self.user.cedula = 1001185389
+ self.user.save()
+ response = self.client.patch(reverse("update_profile"), data)
+ self.assertEqual(response.status_code, 400, response.data)
+ self.assertEqual(
+ response.data["error"],
+ "El correo ingresado no es válido, por favor verifica e intenta de nuevo.",
+ )
+
+ def test_user_creation(self):
+ """Tests that the user creation works as expected."""
+ user = User.objects.create(
+ username="user_test".format(random.randint(0, 999999)),
+ cedula=os.environ["TEST_CEDULA"],
+ first_name="Test",
+ last_name="User",
+ )
+ self.assertEqual(User.objects.count(), 2)
+ self.assertEqual(
+ User.objects.get(pk=user.pk).company_email,
+ os.environ["EMAIL_FOR_TEST"].upper(),
+ )
+
+ def test_get_points(self):
+ """Tests that the get_points endpoint works as expected."""
+ self.create_demo_user()
+ self.user.points = 100
+ self.user.save()
+ response = self.client.get(reverse("get_points"))
+ self.assertEqual(response.status_code, 200, response.data)
+ self.assertEqual(len(response.data), len(User.objects.all()))
+ self.assertEqual(response.data[0]["points"], 100)
+
+ def test_get_points_unauthenticated(self):
+ """Tests that the get_points endpoint returns an error if the user is not authenticated."""
+ self.client.logout()
+ response = self.client.get(reverse("get_points"))
+ self.assertEqual(response.status_code, 401)
+
+ def test_upload_points(self):
+ """Tests that the upload_points endpoint works as expected."""
+ self.create_demo_user(1001185386)
+ self.create_demo_user(1001185390)
+ content = "cedula;puntos\n1001185386;100\n1001185390;200"
+ file = SimpleUploadedFile("points.csv", content.encode("utf-8"))
+ response = self.client.post(reverse("upload_points"), {"file": file})
+ self.assertEqual(response.status_code, 200, response.data)
+ self.assertEqual(User.objects.get(cedula=1001185386).points, 100)
+ self.assertEqual(User.objects.get(cedula=1001185390).points, 200)
+
+ def test_upload_points_unauthenticated(self):
+ """Tests that the upload_points endpoint returns an error if the user is not authenticated."""
+ self.client.logout()
+ response = self.client.post(reverse("upload_points"))
+ self.assertEqual(response.status_code, 401)
+
+ def test_upload_points_wrong_file(self):
+ """Tests that the upload_points endpoint returns an error if the file is not a CSV."""
+ file = SimpleUploadedFile("points.xlsx", b"")
+ response = self.client.post(reverse("upload_points"), {"file": file})
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(
+ response.data["error"],
+ "El archivo debe ser un archivo CSV, por favor verifica e intenta de nuevo.",
+ )
+
+ def test_upload_points_wrong_columns(self):
+ """Tests that the upload_points endpoint returns an error if the file does not have the correct columns."""
+ content = "cedula;score\n1001185386;100\n1001185390;20"
+ file = SimpleUploadedFile("points.csv", content.encode("utf-8"))
+ response = self.client.post(reverse("upload_points"), {"file": file})
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(
+ response.data["error"],
+ "El archivo debe tener dos columnas llamadas 'cedula' y 'puntos', por favor verifica e intenta de nuevo.",
+ )
+
+ def test_upload_points_user_not_found(self):
+ """Tests that the upload_points endpoint returns an error if the user is not found."""
+ self.create_demo_user(1001185386)
+ content = "cedula;puntos\n1001185386;100\n1001185391;200"
+ file = SimpleUploadedFile("points.csv", content.encode("utf-8"))
+ response = self.client.post(reverse("upload_points"), {"file": file})
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(
+ response.data["error"],
+ "Actualización exitosa, pero algunos usuarios no fueron encontrados: 1001185391",
+ )
+ self.assertEqual(Notification.objects.count(), 1)
+
+ def test_upload_points_not_perm(self):
+ """Tests that the upload_points endpoint returns an error if the user does not have the permission."""
+ self.user.user_permissions.clear()
+ content = "cedula;puntos,1001185386;100,1001185390;200"
+ file = SimpleUploadedFile("points.csv", content.encode("utf-8"))
+ response = self.client.post(reverse("upload_points"), {"file": file})
+ self.assertEqual(response.status_code, 403)
+
+ def test_upload_points_read_file(self):
+ """Tests that the upload_points endpoint reads the file correctly."""
+ self.create_demo_user(1001185386)
+ self.create_demo_user(1001185390)
+ with open("utils/excels/puntos-cyc.csv", "rb") as f:
+ file = SimpleUploadedFile("points.csv", f.read())
+ response = self.client.post(reverse("upload_points"), {"file": file})
+ self.assertEqual(response.status_code, 200, response.data)
+ self.assertEqual(User.objects.get(cedula=1001185386).points, 133)
+ self.assertEqual(User.objects.get(cedula=1001185390).points, 20)
diff --git a/INSIGHTSAPI/users/views.py b/INSIGHTSAPI/users/views.py
index e7e487ff..1ba55e8c 100644
--- a/INSIGHTSAPI/users/views.py
+++ b/INSIGHTSAPI/users/views.py
@@ -1,323 +1,323 @@
-import csv
-import logging
-import os
-import sys
-
-import requests
-from django.conf import settings
-from django.contrib.auth.decorators import permission_required
-from django.core.mail import mail_admins
-from django.core.validators import validate_email
-from django.db import connections
-from django.db.models import Q
-from notifications.utils import create_notification
-from rest_framework.decorators import api_view
-from rest_framework.response import Response
-from users.models import User
-
-logger = logging.getLogger("requests")
-
-
-def login_staffnet():
- """Do a request to the StaffNet API to login the user."""
- data = {
- "user": "staffnet",
- "password": os.environ["StaffNetLDAP"],
- }
- if "test" in sys.argv or settings.DEBUG:
- url = "https://staffnet-api-dev.cyc-bpo.com/login"
- else:
- url = "https://staffnet-api.cyc-bpo.com/login"
- response = requests.post(url, json=data)
- if (
- response.status_code != 200
- or "StaffNet" not in response.cookies
- and os.environ.get("StaffNetToken")
- ):
- logger.error("Error logging in StaffNet: {}".format(response.text))
- mail_admins(
- "Error logging in StaffNet",
- "Error logging in StaffNet: {}".format(response.text),
- )
- if os.environ.get("StaffNetToken"):
- # delete the token to try to login again
- del os.environ["StaffNetToken"]
- return None
- os.environ["StaffNetToken"] = response.cookies["StaffNet"]
- return True
-
-
-@api_view(["GET"])
-def get_profile(request):
- """Do a request to the StaffNet API to get the user profile."""
- if not request.user.is_authenticated:
- return Response(
- {
- "error": "No tienes permisos para acceder a esta información, por favor inicia sesión."
- },
- status=401,
- )
-
- if os.environ.get("StaffNetToken") is None:
- if not login_staffnet():
- return Response(
- {
- "error": "Encontramos un error obteniendo tu perfil, por favor intenta más tarde."
- },
- status=500,
- )
- user = request.user
- if "test" in sys.argv or settings.DEBUG:
- url = "https://staffnet-api-dev.cyc-bpo.com/personal-information/{}"
- else:
- url = "https://staffnet-api.cyc-bpo.com/personal-information/{}"
- response = requests.get(
- url.format(user.cedula),
- cookies={"StaffNet": os.environ["StaffNetToken"]},
- )
- if response.status_code != 200 or "error" in response.json():
- # delete the token to try to login again
- del os.environ["StaffNetToken"]
- login_staffnet()
- response = requests.get(
- url.format(user.cedula),
- cookies={"StaffNet": os.environ["StaffNetToken"]},
- )
- if response.status_code != 200 or "error" in response.json():
- logger.error("Error getting user profile: {}".format(response.text))
- return Response(
- {
- "error": "Encontramos un error obteniendo tu perfil, por favor intenta más tarde."
- },
- status=500,
- )
- return Response(response.json())
-
-
-@api_view(["PATCH"])
-def update_profile(request):
- """Do a request to the StaffNet API to update the user profile."""
- if not request.user.is_authenticated:
- return Response(
- {
- "error": "No tienes permisos para acceder a esta información, por favor inicia sesión."
- },
- status=401,
- )
-
- if os.environ.get("StaffNetToken") is None:
- if not login_staffnet():
- return Response(
- {
- "error": "Encontramos un error actualizando tu perfil, por favor intenta después."
- },
- status=500,
- )
- user = request.user
- columns = [
- "estado_civil",
- "hijos",
- "personas_a_cargo",
- "tel_fijo",
- "celular",
- "correo",
- "contacto_emergencia",
- "parentesco",
- "tel_contacto",
- ]
-
- data = {
- "table": "personal_information",
- "cedula": user.cedula,
- }
-
- data["value"] = []
- data["column"] = []
- # Get the values from the request
- for value in columns:
- if request.data.get(value) is not None:
- data["value"].append(request.data.get(value))
- data["column"].append(value)
-
- if not data["value"] or not data["column"]:
- return Response(
- {
- "error": "Alguno de los datos ingresados no es válido, por favor verifica e intenta de nuevo."
- },
- status=400,
- )
-
- if "correo" in data["column"]:
- try:
- validate_email(data["value"][data["column"].index("correo")])
- except Exception:
- return Response(
- {
- "error": "El correo ingresado no es válido, por favor verifica e intenta de nuevo."
- },
- status=400,
- )
-
- if "test" in sys.argv or settings.DEBUG:
- url = "https://staffnet-api-dev.cyc-bpo.com/update"
- else:
- url = "https://staffnet-api.cyc-bpo.com/update"
- # Make the request
- response = requests.patch(
- url,
- json=data,
- cookies={"StaffNet": os.environ["StaffNetToken"]},
- )
- # Check the response
- if response.status_code == 400:
- return Response(
- {
- "error": "No se detectaron cambios en tu perfil, por favor verifica e intenta de nuevo."
- },
- status=400,
- )
- elif response.status_code != 200 or "error" in response.json():
- logger.error("Error updating user profile: {}".format(response.text))
- # delete the token to try to login again
- del os.environ["StaffNetToken"]
- return Response(
- {
- "error": "Encontramos un error actualizando tu perfil, por favor intenta más tarde."
- },
- status=500,
- )
- if "correo" in data["column"]:
- user.email = data["value"][data["column"].index("correo")]
- user.save()
- return Response({"message": "User profile updated"})
-
-
-@api_view(["GET"])
-def get_subordinates(request):
- user_rank = request.user.job_position.rank
- # Get all users that have a lower rank than the current user and are in the same area
- if user_rank >= 4:
- users = User.objects.filter(
- (Q(area=request.user.area) | Q(area__manager=request.user))
- & Q(job_position__rank__lt=user_rank)
- | Q(pk=request.user.pk)
- )
- else:
- users = User.objects.filter(
- Q(area=request.user.area) | Q(area__manager=request.user),
- Q(job_position__rank__lt=user_rank),
- ).order_by("first_name", "last_name", "id")
- # TODO: Refactor this when the migration of StaffNet is done
- # Check if each user is active in StaffNet
- if "test" not in sys.argv and len(users) > 0:
- with connections["staffnet"].cursor() as cursor:
- cursor.execute(
- f"""
- SELECT DISTINCT
- `cedula`
- FROM
- `leave_information`
- WHERE
- `cedula` IN ({",".join([str(user.cedula) for user in users])})
- AND `estado` = TRUE
- """
- )
- active_users = cursor.fetchall()
- active_users = [user[0] for user in active_users]
- users = [user for user in users if int(user.cedula) in active_users]
- # Serialize the users
- data = [{"id": user.id, "name": user.get_full_name()} for user in users]
- return Response(data)
-
-
-@api_view(["POST"])
-@permission_required("users.upload_points", raise_exception=True)
-def upload_points(request):
- """Upload the user points in the database using a CSV file."""
- if not request.user.has_perm("users.upload_points"):
- return Response(
- {
- "error": "No tienes permisos para realizar esta acción, por favor contacta a un administrador."
- },
- status=403,
- )
- file = request.FILES.get("file")
- if not file:
- return Response(
- {
- "error": "No se ha encontrado el archivo, por favor verifica e intenta de nuevo."
- },
- status=400,
- )
- if not file.name.endswith(".csv"):
- return Response(
- {
- "error": "El archivo debe ser un archivo CSV, por favor verifica e intenta de nuevo."
- },
- status=400,
- )
- # Read the file using csv module
- file_data = file.read().decode("utf-8-sig").splitlines()
- lines = csv.reader(file_data, delimiter=";")
- # Check the header
- header = next(lines)
- if len(header) != 2:
- lines = csv.reader(file_data.splitlines(), delimiter=",")
- header = next(lines)
- if header != ["cedula", "puntos"]:
- return Response(
- {
- "error": "El archivo debe tener dos columnas llamadas 'cedula' y 'puntos', por favor verifica e intenta de nuevo."
- },
- status=400,
- )
- # Update the points
- errors = []
- for line in lines:
- cedula = line[0]
- points = line[1]
- if not cedula.isdigit() or not points.isdigit():
- return Response(
- {
- "error": f"{'La cédula' if not cedula.isdigit() else 'Los puntos'} ingresados no son válidos, por favor verifica el valor {cedula if not cedula.isdigit() else points} e intenta de nuevo."
- },
- status=400,
- )
- user = User.objects.filter(cedula=cedula).first()
- if user:
- user.points = points
- user.save()
- else:
- errors.append(cedula)
- if errors:
- message = f"Algunos usuarios no fueron encontrados: {', '.join(errors)}"
- if message.__len__() > 250:
- message = message[:250]
- create_notification(
- "Error actualizando puntos",
- message,
- request.user,
- )
- return Response(
- {
- "error": f"Actualización exitosa, pero algunos usuarios no fueron encontrados: {', '.join(errors)}"
- },
- status=400,
- )
- return Response({"message": "User points updated"})
-
-
-@api_view(["GET"])
-def get_points(request):
- """Check the user points in the database."""
- users = User.objects.all().order_by("-points")
- data = [
- {
- "cedula": user.cedula if user.cedula == request.user.cedula else None,
- "area": user.area.name,
- "name": user.get_full_name(),
- "points": user.points,
- }
- for user in users
- ]
- return Response(data)
+import csv
+import logging
+import os
+import sys
+
+import requests
+from django.conf import settings
+from django.contrib.auth.decorators import permission_required
+from django.core.mail import mail_admins
+from django.core.validators import validate_email
+from django.db import connections
+from django.db.models import Q
+from notifications.utils import create_notification
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from users.models import User
+
+logger = logging.getLogger("requests")
+
+
+def login_staffnet():
+ """Do a request to the StaffNet API to login the user."""
+ data = {
+ "user": "staffnet",
+ "password": os.environ["StaffNetLDAP"],
+ }
+ if "test" in sys.argv or settings.DEBUG:
+ url = "https://staffnet-api-dev.cyc-bpo.com/login"
+ else:
+ url = "https://staffnet-api.cyc-bpo.com/login"
+ response = requests.post(url, json=data)
+ if (
+ response.status_code != 200
+ or "StaffNet" not in response.cookies
+ and os.environ.get("StaffNetToken")
+ ):
+ logger.error("Error logging in StaffNet: {}".format(response.text))
+ mail_admins(
+ "Error logging in StaffNet",
+ "Error logging in StaffNet: {}".format(response.text),
+ )
+ if os.environ.get("StaffNetToken"):
+ # delete the token to try to login again
+ del os.environ["StaffNetToken"]
+ return None
+ os.environ["StaffNetToken"] = response.cookies["StaffNet"]
+ return True
+
+
+@api_view(["GET"])
+def get_profile(request):
+ """Do a request to the StaffNet API to get the user profile."""
+ if not request.user.is_authenticated:
+ return Response(
+ {
+ "error": "No tienes permisos para acceder a esta información, por favor inicia sesión."
+ },
+ status=401,
+ )
+
+ if os.environ.get("StaffNetToken") is None:
+ if not login_staffnet():
+ return Response(
+ {
+ "error": "Encontramos un error obteniendo tu perfil, por favor intenta más tarde."
+ },
+ status=500,
+ )
+ user = request.user
+ if "test" in sys.argv or settings.DEBUG:
+ url = "https://staffnet-api-dev.cyc-bpo.com/personal-information/{}"
+ else:
+ url = "https://staffnet-api.cyc-bpo.com/personal-information/{}"
+ response = requests.get(
+ url.format(user.cedula),
+ cookies={"StaffNet": os.environ["StaffNetToken"]},
+ )
+ if response.status_code != 200 or "error" in response.json():
+ # delete the token to try to login again
+ del os.environ["StaffNetToken"]
+ login_staffnet()
+ response = requests.get(
+ url.format(user.cedula),
+ cookies={"StaffNet": os.environ["StaffNetToken"]},
+ )
+ if response.status_code != 200 or "error" in response.json():
+ logger.error("Error getting user profile: {}".format(response.text))
+ return Response(
+ {
+ "error": "Encontramos un error obteniendo tu perfil, por favor intenta más tarde."
+ },
+ status=500,
+ )
+ return Response(response.json())
+
+
+@api_view(["PATCH"])
+def update_profile(request):
+ """Do a request to the StaffNet API to update the user profile."""
+ if not request.user.is_authenticated:
+ return Response(
+ {
+ "error": "No tienes permisos para acceder a esta información, por favor inicia sesión."
+ },
+ status=401,
+ )
+
+ if os.environ.get("StaffNetToken") is None:
+ if not login_staffnet():
+ return Response(
+ {
+ "error": "Encontramos un error actualizando tu perfil, por favor intenta después."
+ },
+ status=500,
+ )
+ user = request.user
+ columns = [
+ "estado_civil",
+ "hijos",
+ "personas_a_cargo",
+ "tel_fijo",
+ "celular",
+ "correo",
+ "contacto_emergencia",
+ "parentesco",
+ "tel_contacto",
+ ]
+
+ data = {
+ "table": "personal_information",
+ "cedula": user.cedula,
+ }
+
+ data["value"] = []
+ data["column"] = []
+ # Get the values from the request
+ for value in columns:
+ if request.data.get(value) is not None:
+ data["value"].append(request.data.get(value))
+ data["column"].append(value)
+
+ if not data["value"] or not data["column"]:
+ return Response(
+ {
+ "error": "Alguno de los datos ingresados no es válido, por favor verifica e intenta de nuevo."
+ },
+ status=400,
+ )
+
+ if "correo" in data["column"]:
+ try:
+ validate_email(data["value"][data["column"].index("correo")])
+ except Exception:
+ return Response(
+ {
+ "error": "El correo ingresado no es válido, por favor verifica e intenta de nuevo."
+ },
+ status=400,
+ )
+
+ if "test" in sys.argv or settings.DEBUG:
+ url = "https://staffnet-api-dev.cyc-bpo.com/update"
+ else:
+ url = "https://staffnet-api.cyc-bpo.com/update"
+ # Make the request
+ response = requests.patch(
+ url,
+ json=data,
+ cookies={"StaffNet": os.environ["StaffNetToken"]},
+ )
+ # Check the response
+ if response.status_code == 400:
+ return Response(
+ {
+ "error": "No se detectaron cambios en tu perfil, por favor verifica e intenta de nuevo."
+ },
+ status=400,
+ )
+ elif response.status_code != 200 or "error" in response.json():
+ logger.error("Error updating user profile: {}".format(response.text))
+ # delete the token to try to login again
+ del os.environ["StaffNetToken"]
+ return Response(
+ {
+ "error": "Encontramos un error actualizando tu perfil, por favor intenta más tarde."
+ },
+ status=500,
+ )
+ if "correo" in data["column"]:
+ user.email = data["value"][data["column"].index("correo")]
+ user.save()
+ return Response({"message": "User profile updated"})
+
+
+@api_view(["GET"])
+def get_subordinates(request):
+ user_rank = request.user.job_position.rank
+ # Get all users that have a lower rank than the current user and are in the same area
+ if user_rank >= 4:
+ users = User.objects.filter(
+ (Q(area=request.user.area) | Q(area__manager=request.user))
+ & Q(job_position__rank__lt=user_rank)
+ | Q(pk=request.user.pk)
+ )
+ else:
+ users = User.objects.filter(
+ Q(area=request.user.area) | Q(area__manager=request.user),
+ Q(job_position__rank__lt=user_rank),
+ ).order_by("first_name", "last_name", "id")
+ # TODO: Refactor this when the migration of StaffNet is done
+ # Check if each user is active in StaffNet
+ if "test" not in sys.argv and len(users) > 0:
+ with connections["staffnet"].cursor() as cursor:
+ cursor.execute(
+ f"""
+ SELECT DISTINCT
+ `cedula`
+ FROM
+ `leave_information`
+ WHERE
+ `cedula` IN ({",".join([str(user.cedula) for user in users])})
+ AND `estado` = TRUE
+ """
+ )
+ active_users = cursor.fetchall()
+ active_users = [user[0] for user in active_users]
+ users = [user for user in users if int(user.cedula) in active_users]
+ # Serialize the users
+ data = [{"id": user.id, "name": user.get_full_name()} for user in users]
+ return Response(data)
+
+
+@api_view(["POST"])
+@permission_required("users.upload_points", raise_exception=True)
+def upload_points(request):
+ """Upload the user points in the database using a CSV file."""
+ if not request.user.has_perm("users.upload_points"):
+ return Response(
+ {
+ "error": "No tienes permisos para realizar esta acción, por favor contacta a un administrador."
+ },
+ status=403,
+ )
+ file = request.FILES.get("file")
+ if not file:
+ return Response(
+ {
+ "error": "No se ha encontrado el archivo, por favor verifica e intenta de nuevo."
+ },
+ status=400,
+ )
+ if not file.name.endswith(".csv"):
+ return Response(
+ {
+ "error": "El archivo debe ser un archivo CSV, por favor verifica e intenta de nuevo."
+ },
+ status=400,
+ )
+ # Read the file using csv module
+ file_data = file.read().decode("utf-8-sig").splitlines()
+ lines = csv.reader(file_data, delimiter=";")
+ # Check the header
+ header = next(lines)
+ if len(header) != 2:
+ lines = csv.reader(file_data.splitlines(), delimiter=",")
+ header = next(lines)
+ if header != ["cedula", "puntos"]:
+ return Response(
+ {
+ "error": "El archivo debe tener dos columnas llamadas 'cedula' y 'puntos', por favor verifica e intenta de nuevo."
+ },
+ status=400,
+ )
+ # Update the points
+ errors = []
+ for line in lines:
+ cedula = line[0]
+ points = line[1]
+ if not cedula.isdigit() or not points.isdigit():
+ return Response(
+ {
+ "error": f"{'La cédula' if not cedula.isdigit() else 'Los puntos'} ingresados no son válidos, por favor verifica el valor {cedula if not cedula.isdigit() else points} e intenta de nuevo."
+ },
+ status=400,
+ )
+ user = User.objects.filter(cedula=cedula).first()
+ if user:
+ user.points = points
+ user.save()
+ else:
+ errors.append(cedula)
+ if errors:
+ message = f"Algunos usuarios no fueron encontrados: {', '.join(errors)}"
+ if message.__len__() > 250:
+ message = message[:250]
+ create_notification(
+ "Error actualizando puntos",
+ message,
+ request.user,
+ )
+ return Response(
+ {
+ "error": f"Actualización exitosa, pero algunos usuarios no fueron encontrados: {', '.join(errors)}"
+ },
+ status=400,
+ )
+ return Response({"message": "User points updated"})
+
+
+@api_view(["GET"])
+def get_points(request):
+ """Check the user points in the database."""
+ users = User.objects.all().order_by("-points")
+ data = [
+ {
+ "cedula": user.cedula if user.cedula == request.user.cedula else None,
+ "area": user.area.name,
+ "name": user.get_full_name(),
+ "points": user.points,
+ }
+ for user in users
+ ]
+ return Response(data)
diff --git a/INSIGHTSAPI/utils/excels/Call_transfer_list.csv b/INSIGHTSAPI/utils/excels/Call_transfer_list.csv
index e32422a9..8ed7bd6b 100644
--- a/INSIGHTSAPI/utils/excels/Call_transfer_list.csv
+++ b/INSIGHTSAPI/utils/excels/Call_transfer_list.csv
@@ -1,6 +1,6 @@
-FECHA;NUMERO
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
+FECHA;NUMERO
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
diff --git a/INSIGHTSAPI/utils/excels/Call_transfer_list2.csv b/INSIGHTSAPI/utils/excels/Call_transfer_list2.csv
index e32422a9..8ed7bd6b 100644
--- a/INSIGHTSAPI/utils/excels/Call_transfer_list2.csv
+++ b/INSIGHTSAPI/utils/excels/Call_transfer_list2.csv
@@ -1,6 +1,6 @@
-FECHA;NUMERO
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
+FECHA;NUMERO
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
diff --git a/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario.csv b/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario.csv
index e32422a9..8ed7bd6b 100644
--- a/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario.csv
+++ b/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario.csv
@@ -1,6 +1,6 @@
-FECHA;NUMERO
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
+FECHA;NUMERO
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
diff --git a/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario2.csv b/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario2.csv
index e32422a9..8ed7bd6b 100644
--- a/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario2.csv
+++ b/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario2.csv
@@ -1,6 +1,6 @@
-FECHA;NUMERO
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
-5/12/2023;3103233725
+FECHA;NUMERO
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
+5/12/2023;3103233725
diff --git a/INSIGHTSAPI/utils/excels/Nomina.csv b/INSIGHTSAPI/utils/excels/Nomina.csv
index 7d89f61b..9585ec3d 100644
--- a/INSIGHTSAPI/utils/excels/Nomina.csv
+++ b/INSIGHTSAPI/utils/excels/Nomina.csv
@@ -1,4 +1,4 @@
-TITULO DESPRENDIBLE;CEDULA DESPRENDIBLE;NOMBRE DESPRENDIBLE;AREA DESPRENDIBLE;CARGO DESPRENDIBLE; SUELDO DESPRENDIBLE ; DIASLAB DESPRENDIBLE ; QUINCENA DESPRENDIBLE ; SUBSIDIOTRANS DESPRENDIBLE ;RODAMIENTO;HORAS LABORADAS RECARGO NOCTURNO 35%;RECARGO NOCTURNO 35%;HORAS LABORADAS RECARGO NOCTURNO FESTIVO 75%;RECARGO NOCTURNO FESTIVO 75%;HORAS LABORADAS RECARGO DOMINICAL O FESTIVO 110%;RECARGO DOMINICAL O FESTIVO 110%; INCENTIVO DESPRENDIBLE ;PRIMA;CESANTIAS; TOTALDEV DESPRENDIBLE ; APORTESALUD DESPRENDIBLE ; APORTEPENSION DESPRENDIBLE ; RETEFUENTE DESPRENDIBLE ; OTROSDESCUENTOS DESPRENDIBLE ; APSALPEN INCENTIVO ;FONDO SOLIDARIDAD PORCENTAJE;FONDO SOLIDARIDAD; TOTALDEDUC DESPRENDIBLE ; TOTALRECIB DESPRENDIBLE
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;16;14113661;22000;44000;15;140000;17,4;180000;20;250000;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;69000;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;22000;44000;20;140000;17,5;180000;20;250000;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;67000;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;17;14113661;22000;44000;13;140000;17,3;180000;20;250000;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;66000;5060661;9053000
+TITULO DESPRENDIBLE;CEDULA DESPRENDIBLE;NOMBRE DESPRENDIBLE;AREA DESPRENDIBLE;CARGO DESPRENDIBLE; SUELDO DESPRENDIBLE ; DIASLAB DESPRENDIBLE ; QUINCENA DESPRENDIBLE ; SUBSIDIOTRANS DESPRENDIBLE ;RODAMIENTO;HORAS LABORADAS RECARGO NOCTURNO 35%;RECARGO NOCTURNO 35%;HORAS LABORADAS RECARGO NOCTURNO FESTIVO 75%;RECARGO NOCTURNO FESTIVO 75%;HORAS LABORADAS RECARGO DOMINICAL O FESTIVO 110%;RECARGO DOMINICAL O FESTIVO 110%; INCENTIVO DESPRENDIBLE ;PRIMA;CESANTIAS; TOTALDEV DESPRENDIBLE ; APORTESALUD DESPRENDIBLE ; APORTEPENSION DESPRENDIBLE ; RETEFUENTE DESPRENDIBLE ; OTROSDESCUENTOS DESPRENDIBLE ; APSALPEN INCENTIVO ;FONDO SOLIDARIDAD PORCENTAJE;FONDO SOLIDARIDAD; TOTALDEDUC DESPRENDIBLE ; TOTALRECIB DESPRENDIBLE
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;16;14113661;22000;44000;15;140000;17,4;180000;20;250000;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;69000;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;22000;44000;20;140000;17,5;180000;20;250000;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;67000;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;17;14113661;22000;44000;13;140000;17,3;180000;20;250000;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;66000;5060661;9053000
diff --git a/INSIGHTSAPI/utils/excels/Nomina_massive.csv b/INSIGHTSAPI/utils/excels/Nomina_massive.csv
index cc37c5ae..91146820 100644
--- a/INSIGHTSAPI/utils/excels/Nomina_massive.csv
+++ b/INSIGHTSAPI/utils/excels/Nomina_massive.csv
@@ -1,41 +1,41 @@
-TITULO DESPRENDIBLE;CEDULA DESPRENDIBLE;NOMBRE DESPRENDIBLE;AREA DESPRENDIBLE;CARGO DESPRENDIBLE; SUELDO DESPRENDIBLE ; DIASLAB DESPRENDIBLE ; QUINCENA DESPRENDIBLE ; SUBSIDIOTRANS DESPRENDIBLE ;RODAMIENTO;HORAS LABORADAS RECARGO NOCTURNO 35%;RECARGO NOCTURNO 35%;HORAS LABORADAS RECARGO NOCTURNO FESTIVO 75%;RECARGO NOCTURNO FESTIVO 75%;HORAS LABORADAS RECARGO DOMINICAL O FESTIVO 110%;RECARGO DOMINICAL O FESTIVO 110%; INCENTIVO DESPRENDIBLE ;PRIMA;CESANTIAS; TOTALDEV DESPRENDIBLE ; APORTESALUD DESPRENDIBLE ; APORTEPENSION DESPRENDIBLE ; RETEFUENTE DESPRENDIBLE ; OTROSDESCUENTOS DESPRENDIBLE ; APSALPEN INCENTIVO ;FONDO SOLIDARIDAD PORCENTAJE;FONDO SOLIDARIDAD; TOTALDEDUC DESPRENDIBLE ; TOTALRECIB DESPRENDIBLE
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;69000;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;67000;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;66000;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;64333;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;62833;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;61333;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;59833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;58333;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;56833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;55333;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;53833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;52333;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;50833;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;49333;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;47833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;46333;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;44833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;43333;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;41833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;40333;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;38833;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;37333;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;35833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;34333;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;32833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;31333;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;29833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;28333;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;26833;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;25333;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;23833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;22333;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;20833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;19333;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;17833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;16333;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;14833;2372561;7783668
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;13333;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;11833;5060661;9053000
-SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;10333;2372561;7783668
+TITULO DESPRENDIBLE;CEDULA DESPRENDIBLE;NOMBRE DESPRENDIBLE;AREA DESPRENDIBLE;CARGO DESPRENDIBLE; SUELDO DESPRENDIBLE ; DIASLAB DESPRENDIBLE ; QUINCENA DESPRENDIBLE ; SUBSIDIOTRANS DESPRENDIBLE ;RODAMIENTO;HORAS LABORADAS RECARGO NOCTURNO 35%;RECARGO NOCTURNO 35%;HORAS LABORADAS RECARGO NOCTURNO FESTIVO 75%;RECARGO NOCTURNO FESTIVO 75%;HORAS LABORADAS RECARGO DOMINICAL O FESTIVO 110%;RECARGO DOMINICAL O FESTIVO 110%; INCENTIVO DESPRENDIBLE ;PRIMA;CESANTIAS; TOTALDEV DESPRENDIBLE ; APORTESALUD DESPRENDIBLE ; APORTEPENSION DESPRENDIBLE ; RETEFUENTE DESPRENDIBLE ; OTROSDESCUENTOS DESPRENDIBLE ; APSALPEN INCENTIVO ;FONDO SOLIDARIDAD PORCENTAJE;FONDO SOLIDARIDAD; TOTALDEDUC DESPRENDIBLE ; TOTALRECIB DESPRENDIBLE
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;69000;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;67000;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;66000;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;64333;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;62833;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;61333;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;59833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;58333;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;56833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;55333;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;53833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;52333;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;50833;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;49333;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;47833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;46333;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;44833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;43333;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;41833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;40333;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;38833;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;37333;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;35833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;34333;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;32833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;31333;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;29833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;28333;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;26833;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;25333;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;23833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;22333;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;20833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;19333;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;17833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;16333;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;14833;2372561;7783668
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;13333;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;11833;5060661;9053000
+SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;10333;2372561;7783668
diff --git a/INSIGHTSAPI/utils/excels/puntos-cyc.csv b/INSIGHTSAPI/utils/excels/puntos-cyc.csv
index 5f9daa32..665e30c6 100644
--- a/INSIGHTSAPI/utils/excels/puntos-cyc.csv
+++ b/INSIGHTSAPI/utils/excels/puntos-cyc.csv
@@ -1,3 +1,3 @@
-cedula;puntos
-1001185390;20
-1001185386;133
+cedula;puntos
+1001185390;20
+1001185386;133
diff --git a/INSIGHTSAPI/vacation/admin.py b/INSIGHTSAPI/vacation/admin.py
index 7810503d..8ba48ad6 100644
--- a/INSIGHTSAPI/vacation/admin.py
+++ b/INSIGHTSAPI/vacation/admin.py
@@ -1,22 +1,22 @@
-from django.contrib import admin
-
-from .models import VacationRequest
-
-
-@admin.register(VacationRequest)
-class VacationAdmin(admin.ModelAdmin):
- list_display = (
- "user",
- "user_job_position",
- "start_date",
- "end_date",
- # "status",
- "duration",
- "return_date",
- )
- search_fields = ("user__first_name", "user__last_name")
- list_filter = ("status",)
- readonly_fields = (
- "duration",
- "return_date",
- )
+from django.contrib import admin
+
+from .models import VacationRequest
+
+
+@admin.register(VacationRequest)
+class VacationAdmin(admin.ModelAdmin):
+ list_display = (
+ "user",
+ "user_job_position",
+ "start_date",
+ "end_date",
+ # "status",
+ "duration",
+ "return_date",
+ )
+ search_fields = ("user__first_name", "user__last_name")
+ list_filter = ("status",)
+ readonly_fields = (
+ "duration",
+ "return_date",
+ )
diff --git a/INSIGHTSAPI/vacation/migrations/0017_alter_vacationrequest_status.py b/INSIGHTSAPI/vacation/migrations/0017_alter_vacationrequest_status.py
index 88dcf23b..7ce07c5b 100644
--- a/INSIGHTSAPI/vacation/migrations/0017_alter_vacationrequest_status.py
+++ b/INSIGHTSAPI/vacation/migrations/0017_alter_vacationrequest_status.py
@@ -1,18 +1,18 @@
-# Generated by Django 5.0.7 on 2024-09-09 16:51
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('vacation', '0016_vacationrequest_sat_is_working'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='vacationrequest',
- name='status',
- field=models.CharField(choices=[('PENDIENTE', 'PENDIENTE'), ('APROBADA', 'APROBADA'), ('RECHAZADA', 'RECHAZADA'), ('CANCELADA', 'CANCELADA')], default='PENDIENTE', max_length=100),
- ),
- ]
+# Generated by Django 5.0.7 on 2024-09-09 16:51
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('vacation', '0016_vacationrequest_sat_is_working'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='vacationrequest',
+ name='status',
+ field=models.CharField(choices=[('PENDIENTE', 'PENDIENTE'), ('APROBADA', 'APROBADA'), ('RECHAZADA', 'RECHAZADA'), ('CANCELADA', 'CANCELADA')], default='PENDIENTE', max_length=100),
+ ),
+ ]
diff --git a/INSIGHTSAPI/vacation/models.py b/INSIGHTSAPI/vacation/models.py
index 736953af..58339320 100644
--- a/INSIGHTSAPI/vacation/models.py
+++ b/INSIGHTSAPI/vacation/models.py
@@ -1,195 +1,195 @@
-"""This module contains the model for the vacation request """
-
-import pdfkit
-from django.conf import settings
-from django.core.mail import EmailMessage, send_mail
-from django.db import models
-from django.template.loader import render_to_string
-from django.utils import timezone
-
-from notifications.utils import create_notification
-from users.models import User
-from vacation.utils import get_return_date, get_working_days
-
-
-class VacationRequest(models.Model):
- """Model for the vacation request"""
-
- user = models.ForeignKey(
- User, on_delete=models.CASCADE, related_name="vacation_requests"
- )
- start_date = models.DateField()
- end_date = models.DateField()
- sat_is_working = models.BooleanField()
- boss_is_approved = models.BooleanField(null=True, blank=True)
- boss_approved_at = models.DateTimeField(null=True, blank=True)
- manager_is_approved = models.BooleanField(null=True, blank=True)
- manager_approved_at = models.DateTimeField(null=True, blank=True)
- hr_is_approved = models.BooleanField(null=True, blank=True)
- hr_approved_at = models.DateTimeField(null=True, blank=True)
- payroll_is_approved = models.BooleanField(null=True, blank=True)
- payroll_approved_at = models.DateTimeField(null=True, blank=True)
- status = models.CharField(
- choices=[
- ("PENDIENTE", "PENDIENTE"),
- ("APROBADA", "APROBADA"),
- ("RECHAZADA", "RECHAZADA"),
- ("CANCELADA", "CANCELADA"),
- ],
- max_length=100,
- default="PENDIENTE",
- )
- comment = models.TextField(null=True, blank=True)
- created_at = models.DateTimeField(auto_now_add=True)
- # this column is used to store the job position of the user at the time of the request
- user_job_position = models.ForeignKey(
- "hierarchy.JobPosition",
- related_name="vacation_requests",
- on_delete=models.PROTECT,
- )
-
- class Meta:
- """Meta class for the vacation request model."""
-
- permissions = [
- ("payroll_approval", "Can approve payroll"),
- ]
-
- @property
- def duration(self):
- """Return the duration of the vacation request."""
- if self.pk:
- return get_working_days(self.start_date, self.end_date, self.sat_is_working)
- return None
-
- @property
- def return_date(self):
- """Return the return date of the vacation request."""
- if self.pk:
- return get_return_date(self.end_date, self.sat_is_working)
- return None
-
- def __str__(self):
- return f"{self.user} - {self.start_date} - {self.end_date}"
-
- def save(self, *args, **kwargs):
- """Override the save method to update status and create notifications."""
- approbation_fields = {
- "boss_is_approved": "boss_approved_at",
- "manager_is_approved": "manager_approved_at",
- "hr_is_approved": "hr_approved_at",
- "payroll_is_approved": "payroll_approved_at",
- }
-
- # Set the time of approval
- for field, approved_at in approbation_fields.items():
- approbation = getattr(self, field)
- if approbation is not None:
- if not approbation:
- self.status = "RECHAZADA"
- setattr(self, approved_at, timezone.now())
-
- if all(getattr(self, field) for field in approbation_fields):
- self.status = "APROBADA"
- if self.status == "APROBADA":
- create_notification(
- f"Solicitud de vacaciones aprobada",
- f"Tus vacaciones del {self.start_date} al {self.end_date} han sido aprobadas. Esperamos que las disfrutes ⛱!.",
- self.user,
- )
- self.send_approval_email_with_pdf()
- elif self.status == "RECHAZADA":
- message = f"""
- Hola {self.user.get_full_name()} 👋,
-
- Lamentamos informarte que tu solicitud de vacaciones del {self.start_date.strftime("%d de %B del %Y")} al {self.end_date.strftime("%d de %B del %Y")} ha sido rechazada.
-
- Nos vimos en la necesidad de tomar esta decisión debido a: {self.comment}.
-
- Habla con tu gerente o con el departamento de Recursos Humanos si tienes alguna pregunta o necesitas más información. Recuerda que puedes volver a enviar tu solicitud en otro momento.
-
- Saludos cordiales,
- """
- send_mail(
- "Estado de tu solicitud de vacaciones",
- message,
- None,
- [str(self.user.email)],
- )
- create_notification(
- f"Solicitud de vacaciones rechazada",
- f"Tu solicitud de vacaciones del {self.start_date} al {self.end_date} ha sido rechazada.",
- self.user,
- )
- elif self.status == "CANCELADA":
- message = f"""
- Hola {self.user.get_full_name()} 👋,
-
- Has cancelado tu solicitud de vacaciones del {self.start_date.strftime("%d de %B del %Y")} al {self.end_date.strftime("%d de %B del %Y")}.
-
- Saludos cordiales,
- """
- send_mail(
- "Solicitud de cancelación de vacaciones",
- message,
- None,
- [str(self.user.email)],
- )
- create_notification(
- f"Solicitud de vacaciones cancelada",
- f"Tu solicitud de vacaciones del {self.start_date} al {self.end_date} ha sido cancelada.",
- self.user,
- )
- super().save(*args, **kwargs)
-
- def send_approval_email_with_pdf(self):
- # Render the vacation request details in a PDF
- pdf = self.generate_pdf()
-
- # Create the email message
- subject = "Solicitud de vacaciones aprobada"
- message = (
- f"Hola {self.user.get_full_name()} 👋,\n\n"
- "Nos complace informarte que tu solicitud de vacaciones ha sido aprobada.\n\n"
- f"Por favor revisa el archivo adjunto para más detalles sobre tus vacaciones del {self.start_date.strftime('%d de %B del %Y')} al {self.end_date.strftime('%d de %B del %Y')}.\n\n"
- "¡Esperamos que disfrutes tus vacaciones! 🏖️\n\n"
- )
-
- email = EmailMessage(
- subject, message, settings.DEFAULT_FROM_EMAIL, [str(self.user.email)]
- )
-
- # Attach the generated PDF
- email.attach(
- filename="Solicitud de vacaciones.pdf",
- content=pdf,
- mimetype="application/pdf",
- )
-
- # Send the email
- email.send()
-
- def generate_pdf(self):
- # Create context for the PDF
- context = {
- "vacation": self,
- }
-
- # Render the HTML template to a string
- rendered_html = render_to_string("vacation_response.html", context)
-
- # PDF options
- options = {
- "page-size": "Letter",
- "orientation": "Portrait",
- "encoding": "UTF-8",
- "margin-top": "0mm",
- "margin-right": "0mm",
- "margin-bottom": "0mm",
- "margin-left": "0mm",
- }
-
- # Generate the PDF from HTML
- pdf = pdfkit.from_string(rendered_html, False, options=options)
-
- return pdf
+"""This module contains the model for the vacation request """
+
+import pdfkit
+from django.conf import settings
+from django.core.mail import EmailMessage, send_mail
+from django.db import models
+from django.template.loader import render_to_string
+from django.utils import timezone
+
+from notifications.utils import create_notification
+from users.models import User
+from vacation.utils import get_return_date, get_working_days
+
+
+class VacationRequest(models.Model):
+ """Model for the vacation request"""
+
+ user = models.ForeignKey(
+ User, on_delete=models.CASCADE, related_name="vacation_requests"
+ )
+ start_date = models.DateField()
+ end_date = models.DateField()
+ sat_is_working = models.BooleanField()
+ boss_is_approved = models.BooleanField(null=True, blank=True)
+ boss_approved_at = models.DateTimeField(null=True, blank=True)
+ manager_is_approved = models.BooleanField(null=True, blank=True)
+ manager_approved_at = models.DateTimeField(null=True, blank=True)
+ hr_is_approved = models.BooleanField(null=True, blank=True)
+ hr_approved_at = models.DateTimeField(null=True, blank=True)
+ payroll_is_approved = models.BooleanField(null=True, blank=True)
+ payroll_approved_at = models.DateTimeField(null=True, blank=True)
+ status = models.CharField(
+ choices=[
+ ("PENDIENTE", "PENDIENTE"),
+ ("APROBADA", "APROBADA"),
+ ("RECHAZADA", "RECHAZADA"),
+ ("CANCELADA", "CANCELADA"),
+ ],
+ max_length=100,
+ default="PENDIENTE",
+ )
+ comment = models.TextField(null=True, blank=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ # this column is used to store the job position of the user at the time of the request
+ user_job_position = models.ForeignKey(
+ "hierarchy.JobPosition",
+ related_name="vacation_requests",
+ on_delete=models.PROTECT,
+ )
+
+ class Meta:
+ """Meta class for the vacation request model."""
+
+ permissions = [
+ ("payroll_approval", "Can approve payroll"),
+ ]
+
+ @property
+ def duration(self):
+ """Return the duration of the vacation request."""
+ if self.pk:
+ return get_working_days(self.start_date, self.end_date, self.sat_is_working)
+ return None
+
+ @property
+ def return_date(self):
+ """Return the return date of the vacation request."""
+ if self.pk:
+ return get_return_date(self.end_date, self.sat_is_working)
+ return None
+
+ def __str__(self):
+ return f"{self.user} - {self.start_date} - {self.end_date}"
+
+ def save(self, *args, **kwargs):
+ """Override the save method to update status and create notifications."""
+ approbation_fields = {
+ "boss_is_approved": "boss_approved_at",
+ "manager_is_approved": "manager_approved_at",
+ "hr_is_approved": "hr_approved_at",
+ "payroll_is_approved": "payroll_approved_at",
+ }
+
+ # Set the time of approval
+ for field, approved_at in approbation_fields.items():
+ approbation = getattr(self, field)
+ if approbation is not None:
+ if not approbation:
+ self.status = "RECHAZADA"
+ setattr(self, approved_at, timezone.now())
+
+ if all(getattr(self, field) for field in approbation_fields):
+ self.status = "APROBADA"
+ if self.status == "APROBADA":
+ create_notification(
+ f"Solicitud de vacaciones aprobada",
+ f"Tus vacaciones del {self.start_date} al {self.end_date} han sido aprobadas. Esperamos que las disfrutes ⛱!.",
+ self.user,
+ )
+ self.send_approval_email_with_pdf()
+ elif self.status == "RECHAZADA":
+ message = f"""
+ Hola {self.user.get_full_name()} 👋,
+
+ Lamentamos informarte que tu solicitud de vacaciones del {self.start_date.strftime("%d de %B del %Y")} al {self.end_date.strftime("%d de %B del %Y")} ha sido rechazada.
+
+ Nos vimos en la necesidad de tomar esta decisión debido a: {self.comment}.
+
+ Habla con tu gerente o con el departamento de Recursos Humanos si tienes alguna pregunta o necesitas más información. Recuerda que puedes volver a enviar tu solicitud en otro momento.
+
+ Saludos cordiales,
+ """
+ send_mail(
+ "Estado de tu solicitud de vacaciones",
+ message,
+ None,
+ [str(self.user.email)],
+ )
+ create_notification(
+ f"Solicitud de vacaciones rechazada",
+ f"Tu solicitud de vacaciones del {self.start_date} al {self.end_date} ha sido rechazada.",
+ self.user,
+ )
+ elif self.status == "CANCELADA":
+ message = f"""
+ Hola {self.user.get_full_name()} 👋,
+
+ Has cancelado tu solicitud de vacaciones del {self.start_date.strftime("%d de %B del %Y")} al {self.end_date.strftime("%d de %B del %Y")}.
+
+ Saludos cordiales,
+ """
+ send_mail(
+ "Solicitud de cancelación de vacaciones",
+ message,
+ None,
+ [str(self.user.email)],
+ )
+ create_notification(
+ f"Solicitud de vacaciones cancelada",
+ f"Tu solicitud de vacaciones del {self.start_date} al {self.end_date} ha sido cancelada.",
+ self.user,
+ )
+ super().save(*args, **kwargs)
+
+ def send_approval_email_with_pdf(self):
+ # Render the vacation request details in a PDF
+ pdf = self.generate_pdf()
+
+ # Create the email message
+ subject = "Solicitud de vacaciones aprobada"
+ message = (
+ f"Hola {self.user.get_full_name()} 👋,\n\n"
+ "Nos complace informarte que tu solicitud de vacaciones ha sido aprobada.\n\n"
+ f"Por favor revisa el archivo adjunto para más detalles sobre tus vacaciones del {self.start_date.strftime('%d de %B del %Y')} al {self.end_date.strftime('%d de %B del %Y')}.\n\n"
+ "¡Esperamos que disfrutes tus vacaciones! 🏖️\n\n"
+ )
+
+ email = EmailMessage(
+ subject, message, settings.DEFAULT_FROM_EMAIL, [str(self.user.email)]
+ )
+
+ # Attach the generated PDF
+ email.attach(
+ filename="Solicitud de vacaciones.pdf",
+ content=pdf,
+ mimetype="application/pdf",
+ )
+
+ # Send the email
+ email.send()
+
+ def generate_pdf(self):
+ # Create context for the PDF
+ context = {
+ "vacation": self,
+ }
+
+ # Render the HTML template to a string
+ rendered_html = render_to_string("vacation_response.html", context)
+
+ # PDF options
+ options = {
+ "page-size": "Letter",
+ "orientation": "Portrait",
+ "encoding": "UTF-8",
+ "margin-top": "0mm",
+ "margin-right": "0mm",
+ "margin-bottom": "0mm",
+ "margin-left": "0mm",
+ }
+
+ # Generate the PDF from HTML
+ pdf = pdfkit.from_string(rendered_html, False, options=options)
+
+ return pdf
diff --git a/INSIGHTSAPI/vacation/serializers.py b/INSIGHTSAPI/vacation/serializers.py
index 65895fcc..52304e4e 100644
--- a/INSIGHTSAPI/vacation/serializers.py
+++ b/INSIGHTSAPI/vacation/serializers.py
@@ -1,168 +1,168 @@
-"""Serializers for the vacation app."""
-
-from datetime import datetime
-from distutils.util import strtobool
-
-from rest_framework import serializers
-
-from hierarchy.models import JobPosition
-
-from .models import VacationRequest
-from .utils import get_working_days, is_working_day
-
-
-class VacationRequestSerializer(serializers.ModelSerializer):
- """Serializer for the vacation request model."""
-
- user = serializers.HiddenField(default=serializers.CurrentUserDefault())
-
- class Meta:
- """Meta class for the serializer."""
-
- model = VacationRequest
- fields = [
- "id",
- "user",
- "start_date",
- "end_date",
- "created_at",
- "boss_is_approved",
- "boss_approved_at",
- "manager_is_approved",
- "manager_approved_at",
- "hr_is_approved",
- "hr_approved_at",
- "payroll_is_approved",
- "payroll_approved_at",
- "sat_is_working",
- "status",
- "comment",
- "user_job_position",
- ]
- read_only_fields = [
- "boss_approved_at",
- "manager_approved_at",
- "hr_approved_at",
- "payroll_approved_at",
- "created_at",
- "user",
- "user_job_position",
- ]
-
- def to_representation(self, instance):
- """Return the representation of the vacation request."""
- data = super().to_representation(instance)
- data["username"] = instance.user.get_full_name()
- data["user_id"] = instance.user.id
- if "request" in self.context and self.context["request"].user.has_perm("vacation.payroll_approval"):
- data["cedula"] = instance.user.cedula
- data.pop("manager_approved_at")
- data.pop("hr_approved_at")
- data.pop("payroll_approved_at")
- data.pop("user_job_position")
- return data
-
- def validate(self, attrs):
- """Validate the dates of the vacation request."""
- # Check if is a creation or an update
- if not self.instance:
- # Creation
- created_at = datetime.now()
- request = self.context["request"]
- if request.data.get("sat_is_working") is None:
- raise serializers.ValidationError(
- "Debes especificar si trabajas los sábados."
- )
- else:
- try:
- sat_is_working = bool(strtobool(request.data["sat_is_working"]))
- except ValueError:
- raise serializers.ValidationError(
- "Debes especificar si trabajas los sábados o no."
- )
- if not is_working_day(attrs["start_date"], sat_is_working):
- raise serializers.ValidationError(
- "No puedes iniciar tus vacaciones un día no laboral."
- )
- if not is_working_day(attrs["end_date"], sat_is_working):
- raise serializers.ValidationError(
- "No puedes terminar tus vacaciones un día no laboral."
- )
- if request.data["sat_is_working"] == True:
- if attrs["start_date"].weekday() == 5:
- raise serializers.ValidationError(
- "No puedes iniciar tus vacaciones un sábado."
- )
- if (
- get_working_days(attrs["start_date"], attrs["end_date"], sat_is_working)
- > 15
- ):
- raise serializers.ValidationError(
- "No puedes solicitar más de 15 días de vacaciones."
- )
- if (
- created_at.day > 20
- and created_at.month + 1 == attrs["start_date"].month
- ):
- raise serializers.ValidationError(
- "Después del día 20 no puedes solicitar vacaciones para el mes siguiente."
- )
- if (
- attrs["start_date"].month == created_at.month
- and attrs["start_date"].year == created_at.year
- ):
- raise serializers.ValidationError(
- "No puedes solicitar vacaciones para el mes actual."
- )
- if attrs["start_date"] > attrs["end_date"]:
- raise serializers.ValidationError(
- "La fecha de inicio no puede ser mayor a la fecha de fin."
- )
- if attrs["end_date"].weekday() == 6:
- raise serializers.ValidationError(
- "No puedes terminar tus vacaciones un domingo."
- )
- else:
- # Update
- if (
- self.instance.boss_is_approved
- and "status" in attrs
- and attrs["status"] == "CANCELADA"
- ):
- raise serializers.ValidationError(
- "No puedes cancelar una solicitud que ya ha recibido aprobación."
- )
- return attrs
-
- def create(self, validated_data):
- """Create the vacation request."""
- # Remove the is_approved fields from the validated data (security check)
- validated_data.pop("boss_is_approved", None)
- validated_data.pop("manager_is_approved", None)
- validated_data.pop("hr_is_approved", None)
- validated_data.pop("payroll_is_approved", None)
- # Add the user job position to the validated data
- job_position = JobPosition.objects.get(
- id=validated_data["user"].job_position_id
- )
- validated_data["user_job_position"] = job_position
- # Create the vacation request
- vacation_request = super().create(validated_data)
- return vacation_request
-
- def update(self, instance, validated_data):
- """Update the vacation request."""
- allowed_fields = [
- "boss_is_approved",
- "manager_is_approved",
- "hr_is_approved",
- "payroll_is_approved",
- # Status can only be updated to CANCELADA
- "status",
- "comment",
- ]
- for field, value in validated_data.items():
- if field in allowed_fields:
- setattr(instance, field, value)
- instance.save()
- return instance
+"""Serializers for the vacation app."""
+
+from datetime import datetime
+from distutils.util import strtobool
+
+from rest_framework import serializers
+
+from hierarchy.models import JobPosition
+
+from .models import VacationRequest
+from .utils import get_working_days, is_working_day
+
+
+class VacationRequestSerializer(serializers.ModelSerializer):
+ """Serializer for the vacation request model."""
+
+ user = serializers.HiddenField(default=serializers.CurrentUserDefault())
+
+ class Meta:
+ """Meta class for the serializer."""
+
+ model = VacationRequest
+ fields = [
+ "id",
+ "user",
+ "start_date",
+ "end_date",
+ "created_at",
+ "boss_is_approved",
+ "boss_approved_at",
+ "manager_is_approved",
+ "manager_approved_at",
+ "hr_is_approved",
+ "hr_approved_at",
+ "payroll_is_approved",
+ "payroll_approved_at",
+ "sat_is_working",
+ "status",
+ "comment",
+ "user_job_position",
+ ]
+ read_only_fields = [
+ "boss_approved_at",
+ "manager_approved_at",
+ "hr_approved_at",
+ "payroll_approved_at",
+ "created_at",
+ "user",
+ "user_job_position",
+ ]
+
+ def to_representation(self, instance):
+ """Return the representation of the vacation request."""
+ data = super().to_representation(instance)
+ data["username"] = instance.user.get_full_name()
+ data["user_id"] = instance.user.id
+ if "request" in self.context and self.context["request"].user.has_perm("vacation.payroll_approval"):
+ data["cedula"] = instance.user.cedula
+ data.pop("manager_approved_at")
+ data.pop("hr_approved_at")
+ data.pop("payroll_approved_at")
+ data.pop("user_job_position")
+ return data
+
+ def validate(self, attrs):
+ """Validate the dates of the vacation request."""
+ # Check if is a creation or an update
+ if not self.instance:
+ # Creation
+ created_at = datetime.now()
+ request = self.context["request"]
+ if request.data.get("sat_is_working") is None:
+ raise serializers.ValidationError(
+ "Debes especificar si trabajas los sábados."
+ )
+ else:
+ try:
+ sat_is_working = bool(strtobool(request.data["sat_is_working"]))
+ except ValueError:
+ raise serializers.ValidationError(
+ "Debes especificar si trabajas los sábados o no."
+ )
+ if not is_working_day(attrs["start_date"], sat_is_working):
+ raise serializers.ValidationError(
+ "No puedes iniciar tus vacaciones un día no laboral."
+ )
+ if not is_working_day(attrs["end_date"], sat_is_working):
+ raise serializers.ValidationError(
+ "No puedes terminar tus vacaciones un día no laboral."
+ )
+ if request.data["sat_is_working"] == True:
+ if attrs["start_date"].weekday() == 5:
+ raise serializers.ValidationError(
+ "No puedes iniciar tus vacaciones un sábado."
+ )
+ if (
+ get_working_days(attrs["start_date"], attrs["end_date"], sat_is_working)
+ > 15
+ ):
+ raise serializers.ValidationError(
+ "No puedes solicitar más de 15 días de vacaciones."
+ )
+ if (
+ created_at.day > 20
+ and created_at.month + 1 == attrs["start_date"].month
+ ):
+ raise serializers.ValidationError(
+ "Después del día 20 no puedes solicitar vacaciones para el mes siguiente."
+ )
+ if (
+ attrs["start_date"].month == created_at.month
+ and attrs["start_date"].year == created_at.year
+ ):
+ raise serializers.ValidationError(
+ "No puedes solicitar vacaciones para el mes actual."
+ )
+ if attrs["start_date"] > attrs["end_date"]:
+ raise serializers.ValidationError(
+ "La fecha de inicio no puede ser mayor a la fecha de fin."
+ )
+ if attrs["end_date"].weekday() == 6:
+ raise serializers.ValidationError(
+ "No puedes terminar tus vacaciones un domingo."
+ )
+ else:
+ # Update
+ if (
+ self.instance.boss_is_approved
+ and "status" in attrs
+ and attrs["status"] == "CANCELADA"
+ ):
+ raise serializers.ValidationError(
+ "No puedes cancelar una solicitud que ya ha recibido aprobación."
+ )
+ return attrs
+
+ def create(self, validated_data):
+ """Create the vacation request."""
+ # Remove the is_approved fields from the validated data (security check)
+ validated_data.pop("boss_is_approved", None)
+ validated_data.pop("manager_is_approved", None)
+ validated_data.pop("hr_is_approved", None)
+ validated_data.pop("payroll_is_approved", None)
+ # Add the user job position to the validated data
+ job_position = JobPosition.objects.get(
+ id=validated_data["user"].job_position_id
+ )
+ validated_data["user_job_position"] = job_position
+ # Create the vacation request
+ vacation_request = super().create(validated_data)
+ return vacation_request
+
+ def update(self, instance, validated_data):
+ """Update the vacation request."""
+ allowed_fields = [
+ "boss_is_approved",
+ "manager_is_approved",
+ "hr_is_approved",
+ "payroll_is_approved",
+ # Status can only be updated to CANCELADA
+ "status",
+ "comment",
+ ]
+ for field, value in validated_data.items():
+ if field in allowed_fields:
+ setattr(instance, field, value)
+ instance.save()
+ return instance
diff --git a/INSIGHTSAPI/vacation/templates/vacation_response.html b/INSIGHTSAPI/vacation/templates/vacation_response.html
index b43ce268..70b4b4d8 100644
--- a/INSIGHTSAPI/vacation/templates/vacation_response.html
+++ b/INSIGHTSAPI/vacation/templates/vacation_response.html
@@ -1,141 +1,141 @@
-
-
-
-
-
-
- Respuesta a Solicitud de Vacaciones
-
-
-
-
-
- Bogotá D.C.;
- {% if vacation.payroll_approved_at %}
- {{ vacation.payroll_approved_at|date:"d \\d\\e F \\d\\e Y" }}
- {% elif vacation.hr_approved_at %}
- {{ vacation.hr_approved_at|date:"d \\d\\e F \\d\\e Y" }}
- {% elif vacation.manager_approved_at %}
- {{ vacation.manager_approved_at|date:"d \\d\\e F \\d\\e Y" }}
- {% elif vacation.boss_approved_at %}
- {{ vacation.boss_approved_at|date:"d \\d\\e F \\d\\e Y" }}
- {% else %}
- No approval date available
- {% endif %}
-
-
-
-
-
Señor/a:
-
{{vacation.user}}
-
{{vacation.user_job_position}} - Bogotá
-
De. Gestión Humana
-
- REF: aprobación de vacaciones mes de {{ vacation.start_date|date:"F" }} de {{ vacation.start_date|date:"Y" }}
-
-
- {% if vacation.status == 'APROBADA' %}
-
- Me permito informarle que en respuesta a su comunicación recibida el día {{ vacation.created_at|date }}, usted podrá disfrutar de sus vacaciones
- a partir del día {{ vacation.start_date }}, retomando actividades laborales el día
- {{ vacation.return_date }}.
-
-
Espero disfrute con agrado sus merecidas vacaciones.
- {% else %}
-
- Lamentamos informarle que su solicitud de vacaciones recibida el día {{ vacation.created_at|date }} no ha sido aprobada debido al siguiente motivo:
-
+ Bogotá D.C.;
+ {% if vacation.payroll_approved_at %}
+ {{ vacation.payroll_approved_at|date:"d \\d\\e F \\d\\e Y" }}
+ {% elif vacation.hr_approved_at %}
+ {{ vacation.hr_approved_at|date:"d \\d\\e F \\d\\e Y" }}
+ {% elif vacation.manager_approved_at %}
+ {{ vacation.manager_approved_at|date:"d \\d\\e F \\d\\e Y" }}
+ {% elif vacation.boss_approved_at %}
+ {{ vacation.boss_approved_at|date:"d \\d\\e F \\d\\e Y" }}
+ {% else %}
+ No approval date available
+ {% endif %}
+
+
+
+
+
Señor/a:
+
{{vacation.user}}
+
{{vacation.user_job_position}} - Bogotá
+
De. Gestión Humana
+
+ REF: aprobación de vacaciones mes de {{ vacation.start_date|date:"F" }} de {{ vacation.start_date|date:"Y" }}
+
+
+ {% if vacation.status == 'APROBADA' %}
+
+ Me permito informarle que en respuesta a su comunicación recibida el día {{ vacation.created_at|date }}, usted podrá disfrutar de sus vacaciones
+ a partir del día {{ vacation.start_date }}, retomando actividades laborales el día
+ {{ vacation.return_date }}.
+
+
Espero disfrute con agrado sus merecidas vacaciones.
+ {% else %}
+
+ Lamentamos informarle que su solicitud de vacaciones recibida el día {{ vacation.created_at|date }} no ha sido aprobada debido al siguiente motivo:
+
+
+
\ No newline at end of file
diff --git a/INSIGHTSAPI/vacation/tests.py b/INSIGHTSAPI/vacation/tests.py
index 2eb4cd07..5d863de7 100644
--- a/INSIGHTSAPI/vacation/tests.py
+++ b/INSIGHTSAPI/vacation/tests.py
@@ -1,597 +1,608 @@
-"""This file contains the tests for the vacation model."""
-
-from datetime import datetime
-
-from django.contrib.auth.models import Permission
-from django.db.models import Q
-from django.test import TestCase, override_settings
-from django.urls import reverse
-from freezegun import freeze_time
-from rest_framework import status
-
-from hierarchy.models import Area
-from services.tests import BaseTestCase
-from users.models import User
-
-from .models import VacationRequest
-from .serializers import VacationRequestSerializer
-from .utils import get_return_date, get_working_days, is_working_day
-
-
-class WorkingDayTestCase(TestCase):
- """Test module for working day utility functions."""
-
- def test_is_working_day(self):
- """Test the is_working_day function."""
- self.assertTrue(is_working_day("2024-01-02", True))
- self.assertFalse(is_working_day("2024-01-01", True))
- self.assertTrue(is_working_day("2024-01-05", True))
- self.assertTrue(is_working_day("2024-01-06", True))
- self.assertFalse(is_working_day("2024-01-06", False))
-
- def test_get_working_days_no_sat(self):
- """Test the get_working_days function."""
- self.assertEqual(get_working_days("2024-01-02", "2024-01-05", False), 4)
- self.assertEqual(get_working_days("2024-01-01", "2024-01-05", False), 4)
- # The 8th is a holiday
- self.assertEqual(get_working_days("2024-01-01", "2024-01-09", False), 5)
- self.assertEqual(get_working_days("2024-01-01", "2024-01-23", False), 15)
- self.assertEqual(get_working_days("2024-12-09", "2024-12-27", False), 14)
-
- def test_get_working_days_sat(self):
- """Test the get_working_days function with Saturdays."""
- self.assertEqual(get_working_days("2024-01-02", "2024-01-05", True), 4)
- self.assertEqual(get_working_days("2024-01-01", "2024-01-05", True), 4)
- # The 8th is a holiday
- self.assertEqual(get_working_days("2024-01-01", "2024-01-09", True), 6)
- self.assertEqual(get_working_days("2024-01-01", "2024-01-19", True), 15)
-
- def test_get_return_date(self):
- """Test the get_return_date function."""
- self.assertEqual(get_return_date("2024-08-30", False), datetime(2024, 9, 2))
- self.assertEqual(get_return_date("2024-08-30", True), datetime(2024, 8, 31))
- self.assertEqual(get_return_date("2024-09-06", False), datetime(2024, 9, 9))
- self.assertEqual(get_return_date("2024-12-31", True), datetime(2025, 1, 2))
-
-
-@override_settings(DEFAULT_FILE_STORAGE="django.core.files.storage.InMemoryStorage")
-class VacationRequestModelTestCase(BaseTestCase):
- """Test module for VacationRequest model."""
-
- def setUp(self):
- """Create a user and a vacation request."""
- super().setUp()
- self.test_user = self.create_demo_user()
- self.user.job_position.rank = 2
- self.user.job_position.save()
- self.user.area = self.test_user.area
- self.user.save()
- self.permission = Permission.objects.get(codename="payroll_approval")
- self.vacation_request = {
- "start_date": "2024-01-02",
- "end_date": "2024-01-18",
- }
- self.vacation_request_user = {
- "start_date": "2024-01-02",
- "end_date": "2024-01-18",
- "user": self.test_user,
- "user_job_position": self.test_user.job_position,
- "sat_is_working": True,
- }
-
- def test_vacation_create(self):
- """Test creating a vacation endpoint."""
- self.vacation_request["hr_is_approved"] = True # This is just a check
- self.vacation_request["sat_is_working"] = False
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)
- self.assertEqual(response.data["hr_is_approved"], None)
- self.assertEqual(response.data["status"], "PENDIENTE")
- self.assertEqual(response.data["user_id"], self.user.id)
- self.assertEqual(response.data.get("user_job_position"), None)
- self.assertEqual(response.data["start_date"], "2024-01-02")
- self.assertEqual(response.data["end_date"], "2024-01-18")
- vacation = VacationRequest.objects.get(pk=response.data["id"])
- self.assertEqual(vacation.user_job_position, self.user.job_position)
- self.assertEqual(vacation.sat_is_working, False)
- self.assertEqual(vacation.duration, 12)
-
- def test_vacation_create_no_sat_is_working(self):
- """Test creating a vacation without sat_is_working."""
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(
- response.status_code, status.HTTP_400_BAD_REQUEST, response.data
- )
- self.assertEqual(
- response.data["non_field_errors"][0],
- "Debes especificar si trabajas los sábados.",
- )
-
- @freeze_time("2024-07-01 10:00:00")
- def test_vacation_create_same_month(self):
- """Test creating a vacation that spans two months."""
- super().setUp()
- self.vacation_request["sat_is_working"] = False
- self.vacation_request["start_date"] = "2024-07-22"
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(
- response.status_code, status.HTTP_400_BAD_REQUEST, response.data
- )
- self.assertEqual(
- response.data["non_field_errors"][0],
- "No puedes solicitar vacaciones para el mes actual.",
- )
-
- def test_vacation_list_user(self):
- """Test listing all vacations endpoint for a user."""
- VacationRequest.objects.create(**self.vacation_request_user)
- self.vacation_request_user["user"] = self.user
- VacationRequest.objects.create(**self.vacation_request_user)
- self.user.job_position.rank = 1
- self.user.job_position.save()
- response = self.client.get(reverse("vacation-list"))
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 1)
-
- def test_vacation_list_boss(self):
- """Test listing all vacations endpoint for a boss."""
- self.user.job_position.rank = 2
- self.user.job_position.save()
- self.test_user.area = self.user.area
- self.test_user.save()
- VacationRequest.objects.create(**self.vacation_request_user)
- VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.get(reverse("vacation-list"))
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 2)
-
- def test_vacation_list_manager(self):
- """Test listing all vacations endpoint for a manager."""
- self.user.job_position.rank = 5
- self.user.job_position.save()
- self.test_user.area.manager = self.user
- self.test_user.area.save()
- VacationRequest.objects.create(**self.vacation_request_user)
- VacationRequest.objects.create(**self.vacation_request_user)
- # Change the area of the test user to match the user's area
- demo_user_admin = self.create_demo_user_admin()
- demo_user_admin.area = self.user.area
- demo_user_admin.save()
- demo_user_admin.job_position.rank = 1
- demo_user_admin.job_position.save()
- VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.get(reverse("vacation-list"))
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 3, response.data)
-
- def test_vacation_list_manager_multiple_areas(self):
- """Test listing all vacations endpoint for a manager with multiple areas."""
- self.test_user.area.manager = self.user
- self.test_user.area.save()
- self.user.area = Area.objects.create(name="Test Area 2", manager=self.user)
- self.user.save()
- # Check that the user has a different area than the manager
- self.assertNotEqual(self.test_user.area, self.user.area)
- VacationRequest.objects.create(**self.vacation_request_user)
- self.create_demo_user()
- Area.objects.create(name="Test Area", manager=self.user)
- VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.get(reverse("vacation-list"))
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 2)
-
- def test_vacation_list_payroll(self):
- """Test listing all vacations endpoint for payroll."""
- self.user.user_permissions.add(self.permission)
- VacationRequest.objects.create(**self.vacation_request_user)
- VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.get(reverse("vacation-list"))
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 2)
-
- def test_vacation_list_hr(self):
- """Test listing all vacations endpoint for HR."""
- self.user.job_position.name = "GERENTE DE GESTION HUMANA"
- self.user.job_position.save()
- VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.get(reverse("vacation-list"))
- vacation_requests = VacationRequest.objects.all()
- serializer = VacationRequestSerializer(vacation_requests, many=True)
- self.assertEqual(response.data, serializer.data)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_vacation_retrieve(self):
- """Test retrieving a vacation endpoint."""
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.get(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk})
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(
- response.data["start_date"], self.vacation_request["start_date"]
- )
- self.assertEqual(response.data["end_date"], self.vacation_request["end_date"])
-
- def test_vacation_create_end_before_start(self):
- """Test creating a vacation with the end date before the start date."""
- self.vacation_request["end_date"] = "2021-01-04"
- self.vacation_request["sat_is_working"] = False
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(
- response.data["non_field_errors"][0],
- "La fecha de inicio no puede ser mayor a la fecha de fin.",
- )
-
- def test_vacation_owner_cancel_approved(self):
- """Test the owner cancelling an approved vacation."""
- self.vacation_request_user["boss_is_approved"] = True
- self.vacation_request_user["user"] = self.user
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"status": "CANCELADA"},
- )
- self.assertEqual(
- response.status_code, status.HTTP_400_BAD_REQUEST, response.data
- )
- self.assertEqual(
- str(response.data["non_field_errors"][0]),
- "No puedes cancelar una solicitud que ya ha recibido aprobación.",
- )
-
- def test_vacation_cancel_no_owner(self):
- """Test cancelling a vacation without being the owner."""
- self.vacation_request_user["user"] = self.test_user
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"status": "CANCELADA"},
- )
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
-
- def test_vacation_boss_approve(self):
- """Test the boss approving a vacation."""
- self.user.job_position.rank = 2
- self.user.job_position.save()
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"boss_is_approved": True},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
- self.assertTrue(response.data["boss_is_approved"])
- vacation_object.refresh_from_db()
- self.assertIsNotNone(vacation_object.boss_approved_at)
-
- def test_vacation_manager_approve(self):
- """Test the manager approving a vacation."""
- self.user.job_position.rank = 5
- self.user.job_position.save()
- self.test_user.job_position.name = "GERENTE DE GESTION HUMANA"
- self.test_user.job_position.save()
- self.vacation_request_user["boss_is_approved"] = True
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"manager_is_approved": True},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
- self.assertTrue(response.data["manager_is_approved"])
- vacation_object.refresh_from_db()
- self.assertIsNotNone(vacation_object.manager_approved_at)
-
- def test_vacation_manager_reject(self):
- """Test the manager rejecting a vacation."""
- self.user.job_position.rank = 5
- self.user.job_position.save()
- self.vacation_request_user["boss_is_approved"] = True
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"manager_is_approved": False},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
- self.assertFalse(response.data["manager_is_approved"])
- self.assertEqual(response.data["status"], "RECHAZADA")
- vacation_object.refresh_from_db()
- self.assertIsNotNone(vacation_object.manager_approved_at)
-
- def test_vacation_manager_approve_before_boss(self):
- """Test the manager approving a vacation before the boss."""
- self.user.job_position.rank = 5
- self.user.job_position.save()
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"manager_is_approved": True},
- )
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
-
- def test_vacation_manager_approve_no_manager(self):
- """Test the manager approving a vacation without being a manager."""
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"manager_is_approved": True},
- )
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
-
- def test_vacation_hr_approve(self):
- """Test HR approving a vacation."""
- self.user.job_position.name = "GERENTE DE GESTION HUMANA"
- self.user.job_position.save()
- test_user = self.create_demo_user()
- test_user.user_permissions.add(self.permission)
- self.vacation_request_user["manager_is_approved"] = True
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"hr_is_approved": True},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
- self.assertTrue(response.data["hr_is_approved"])
- vacation_object.refresh_from_db()
- self.assertIsNotNone(vacation_object.hr_approved_at)
-
- def test_vacation_hr_reject(self):
- """Test HR rejecting a vacation."""
- self.user.job_position.name = "GERENTE DE GESTION HUMANA"
- self.user.job_position.save()
- self.vacation_request_user["manager_is_approved"] = True
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"hr_is_approved": False},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
- self.assertFalse(response.data["hr_is_approved"])
- self.assertEqual(response.data["status"], "RECHAZADA")
- vacation_object.refresh_from_db()
- self.assertIsNotNone(vacation_object.hr_approved_at)
-
- def test_vacation_hr_approve_before_manager(self):
- """Test HR approving a vacation before the manager."""
- self.user.job_position.name = "GERENTE DE GESTION HUMANA"
- self.user.job_position.save()
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"hr_is_approved": True},
- )
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
-
- def test_vacation_hr_approve_no_hr(self):
- """Test HR approving a vacation without being an HR."""
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"hr_is_approved": True},
- )
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
-
- def test_vacation_payroll_approve(self):
- """Test payroll approving a vacation."""
- self.user.user_permissions.add(self.permission)
- self.vacation_request_user["hr_is_approved"] = True
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"payroll_is_approved": True},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
- self.assertTrue(response.data["payroll_is_approved"])
- vacation_object.refresh_from_db()
- self.assertIsNotNone(vacation_object.payroll_approved_at)
-
- def test_vacation_payroll_reject(self):
- """Test payroll rejecting a vacation."""
- self.user.user_permissions.add(self.permission)
- self.vacation_request_user["hr_is_approved"] = True
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"payroll_is_approved": False},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
- self.assertFalse(response.data["payroll_is_approved"])
- self.assertEqual(response.data["status"], "RECHAZADA")
- vacation_object.refresh_from_db()
- self.assertIsNotNone(vacation_object.payroll_approved_at)
-
- def test_vacation_payroll_approve_before_hr(self):
- """Test payroll approving a vacation before HR."""
- self.user.user_permissions.add(self.permission)
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"payroll_is_approved": True},
- )
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
-
- def test_vacation_payroll_approve_no_payroll(self):
- """Test payroll approving a vacation without being in payroll."""
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.patch(
- reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
- {"payroll_is_approved": True},
- )
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
-
- @freeze_time("2024-07-21 10:00:00")
- def test_validate_vacation_request_after_20th(self):
- """Test the validation of a vacation request after the 20th."""
- super().setUp()
- self.vacation_request["sat_is_working"] = False
- self.vacation_request["start_date"] = "2024-08-12"
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(
- response.status_code, status.HTTP_400_BAD_REQUEST, response.data
- )
- self.assertEqual(
- response.data["non_field_errors"][0],
- "Después del día 20 no puedes solicitar vacaciones para el mes siguiente.",
- )
-
- def test_validate_vacation_request_not_working_day(self):
- """Test the validation of a vacation request on a non-working day."""
- self.vacation_request["sat_is_working"] = False
- self.vacation_request["start_date"] = "2024-01-01"
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(
- response.status_code, status.HTTP_400_BAD_REQUEST, response.data
- )
- self.assertEqual(
- response.data["non_field_errors"][0],
- "No puedes iniciar tus vacaciones un día no laboral.",
- )
-
- def test_validate_vacation_request_not_working_day_sat(self):
- """Test the validation of a vacation request on a Saturday."""
- self.vacation_request["sat_is_working"] = False
- self.vacation_request["start_date"] = "2024-05-04"
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(
- response.status_code, status.HTTP_400_BAD_REQUEST, response.data
- )
- # This is fine because if the user doesn't work on Saturdays, they can't start their vacation on a Saturday
- self.assertEqual(
- response.data["non_field_errors"][0],
- "No puedes iniciar tus vacaciones un día no laboral.",
- )
-
- def test_validate_vacation_request_not_working_day_sat_working(self):
- """Test the validation of a vacation request on a Saturday with working Saturdays."""
- self.vacation_request["sat_is_working"] = True
- self.vacation_request["start_date"] = "2024-05-04"
- self.vacation_request["end_date"] = "2024-05-06"
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)
-
- def test_validate_vacation_request_not_working_day_end(self):
- """Test the validation of a vacation request on a non-working day."""
- self.vacation_request["sat_is_working"] = False
- self.vacation_request["end_date"] = "2024-01-01"
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(
- response.status_code, status.HTTP_400_BAD_REQUEST, response.data
- )
- self.assertEqual(
- response.data["non_field_errors"][0],
- "No puedes terminar tus vacaciones un día no laboral.",
- )
-
- def test_validate_vacation_request_not_working_day_sat_end(self):
- """Test the validation of a vacation request on a Saturday."""
- self.vacation_request["sat_is_working"] = False
- self.vacation_request["end_date"] = "2024-05-04"
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(
- response.status_code, status.HTTP_400_BAD_REQUEST, response.data
- )
- # This is fine because if the user doesn't work on Saturdays, they can't end their vacation on a Saturday
- self.assertEqual(
- response.data["non_field_errors"][0],
- "No puedes terminar tus vacaciones un día no laboral.",
- )
-
- def test_validate_vacation_request_not_working_day_sat_working_end(self):
- """Test the validation of a vacation request on a Saturday with working Saturdays."""
- self.vacation_request["sat_is_working"] = True
- self.vacation_request["start_date"] = "2024-05-03"
- self.vacation_request["end_date"] = "2024-05-04"
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)
-
- def test_validate_vacation_request_more_than_15_days(self):
- """Test the validation of a vacation request with more than 15 days."""
- self.vacation_request["sat_is_working"] = False
- self.vacation_request["end_date"] = "2024-01-24"
- response = self.client.post(
- reverse("vacation-list"),
- self.vacation_request,
- )
- self.assertEqual(
- response.status_code, status.HTTP_400_BAD_REQUEST, response.data
- )
- self.assertEqual(
- response.data["non_field_errors"][0],
- "No puedes solicitar más de 15 días de vacaciones.",
- )
-
- def test_get_vacation_request(self):
- """Test getting the vacation request PDF."""
- self.user.user_permissions.add(self.permission)
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.get(
- reverse("vacation-get-request", kwargs={"pk": vacation_object.pk})
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response["Content-Type"], "application/pdf")
-
- def test_get_vacation_request_no_permission(self):
- """Test getting the vacation request PDF without permission."""
- self.user.job_position.rank = 1
- self.user.job_position.save()
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.get(
- reverse("vacation-get-request", kwargs={"pk": vacation_object.pk})
- )
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- def test_get_vacation_request_manager(self):
- """Test getting the vacation request PDF as a manager."""
- self.user.job_position.rank = 5
- self.user.job_position.save()
- self.test_user.area.manager = self.user
- self.test_user.area.save()
- vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.get(
- reverse("vacation-get-request", kwargs={"pk": vacation_object.pk})
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response["Content-Type"], "application/pdf")
-
- def test_get_manage_multiple_children(self):
- """Test managing multiple children."""
- self.test_user.area.parent = self.user.area
- self.test_user.area.save()
- VacationRequest.objects.create(**self.vacation_request_user)
- response = self.client.get(
- reverse("vacation-list"),
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 1)
+"""This file contains the tests for the vacation model."""
+
+from datetime import datetime
+
+from django.contrib.auth.models import Permission
+from django.db.models import Q
+from django.test import TestCase, override_settings
+from django.urls import reverse
+from freezegun import freeze_time
+from rest_framework import status
+
+from hierarchy.models import Area
+from services.tests import BaseTestCase
+from users.models import User
+
+from .models import VacationRequest
+from .serializers import VacationRequestSerializer
+from .utils import get_return_date, get_working_days, is_working_day
+
+
+class WorkingDayTestCase(TestCase):
+ """Test module for working day utility functions."""
+
+ def test_is_working_day(self):
+ """Test the is_working_day function."""
+ self.assertTrue(is_working_day("2024-01-02", True))
+ self.assertFalse(is_working_day("2024-01-01", True))
+ self.assertTrue(is_working_day("2024-01-05", True))
+ self.assertTrue(is_working_day("2024-01-06", True))
+ self.assertFalse(is_working_day("2024-01-06", False))
+
+ def test_get_working_days_no_sat(self):
+ """Test the get_working_days function."""
+ self.assertEqual(get_working_days("2024-01-02", "2024-01-05", False), 4)
+ self.assertEqual(get_working_days("2024-01-01", "2024-01-05", False), 4)
+ # The 8th is a holiday
+ self.assertEqual(get_working_days("2024-01-01", "2024-01-09", False), 5)
+ self.assertEqual(get_working_days("2024-01-01", "2024-01-23", False), 15)
+ self.assertEqual(get_working_days("2024-12-09", "2024-12-27", False), 14)
+
+ def test_get_working_days_sat(self):
+ """Test the get_working_days function with Saturdays."""
+ self.assertEqual(get_working_days("2024-01-02", "2024-01-05", True), 4)
+ self.assertEqual(get_working_days("2024-01-01", "2024-01-05", True), 4)
+ # The 8th is a holiday
+ self.assertEqual(get_working_days("2024-01-01", "2024-01-09", True), 6)
+ self.assertEqual(get_working_days("2024-01-01", "2024-01-19", True), 15)
+
+ def test_get_return_date(self):
+ """Test the get_return_date function."""
+ self.assertEqual(get_return_date("2024-08-30", False), datetime(2024, 9, 2))
+ self.assertEqual(get_return_date("2024-08-30", True), datetime(2024, 8, 31))
+ self.assertEqual(get_return_date("2024-09-06", False), datetime(2024, 9, 9))
+ self.assertEqual(get_return_date("2024-12-31", True), datetime(2025, 1, 2))
+
+
+@override_settings(DEFAULT_FILE_STORAGE="django.core.files.storage.InMemoryStorage")
+class VacationRequestModelTestCase(BaseTestCase):
+ """Test module for VacationRequest model."""
+
+ def setUp(self):
+ """Create a user and a vacation request."""
+ super().setUp()
+ self.test_user = self.create_demo_user()
+ self.user.job_position.rank = 2
+ self.user.job_position.save()
+ self.user.area = self.test_user.area
+ self.user.save()
+ self.permission = Permission.objects.get(codename="payroll_approval")
+ self.vacation_request = {
+ "start_date": "2024-01-02",
+ "end_date": "2024-01-18",
+ }
+ self.vacation_request_user = {
+ "start_date": "2024-01-02",
+ "end_date": "2024-01-18",
+ "user": self.test_user,
+ "user_job_position": self.test_user.job_position,
+ "sat_is_working": True,
+ }
+
+ def test_vacation_create(self):
+ """Test creating a vacation endpoint."""
+ self.vacation_request["hr_is_approved"] = True # This is just a check
+ self.vacation_request["sat_is_working"] = False
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)
+ self.assertEqual(response.data["hr_is_approved"], None)
+ self.assertEqual(response.data["status"], "PENDIENTE")
+ self.assertEqual(response.data["user_id"], self.user.id)
+ self.assertEqual(response.data.get("user_job_position"), None)
+ self.assertEqual(response.data["start_date"], "2024-01-02")
+ self.assertEqual(response.data["end_date"], "2024-01-18")
+ vacation = VacationRequest.objects.get(pk=response.data["id"])
+ self.assertEqual(vacation.user_job_position, self.user.job_position)
+ self.assertEqual(vacation.sat_is_working, False)
+ self.assertEqual(vacation.duration, 12)
+
+ def test_vacation_create_no_sat_is_working(self):
+ """Test creating a vacation without sat_is_working."""
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(
+ response.status_code, status.HTTP_400_BAD_REQUEST, response.data
+ )
+ self.assertEqual(
+ response.data["non_field_errors"][0],
+ "Debes especificar si trabajas los sábados.",
+ )
+
+ @freeze_time("2024-07-01 10:00:00")
+ def test_vacation_create_same_month(self):
+ """Test creating a vacation that spans two months."""
+ super().setUp()
+ self.vacation_request["sat_is_working"] = False
+ self.vacation_request["start_date"] = "2024-07-22"
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(
+ response.status_code, status.HTTP_400_BAD_REQUEST, response.data
+ )
+ self.assertEqual(
+ response.data["non_field_errors"][0],
+ "No puedes solicitar vacaciones para el mes actual.",
+ )
+
+ def test_vacation_list_user(self):
+ """Test listing all vacations endpoint for a user."""
+ VacationRequest.objects.create(**self.vacation_request_user)
+ self.vacation_request_user["user"] = self.user
+ VacationRequest.objects.create(**self.vacation_request_user)
+ self.user.job_position.rank = 1
+ self.user.job_position.save()
+ response = self.client.get(reverse("vacation-list"))
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+
+ def test_vacation_list_boss(self):
+ """Test listing all vacations endpoint for a boss."""
+ self.user.job_position.rank = 2
+ self.user.job_position.save()
+ self.test_user.area = self.user.area
+ self.test_user.save()
+ VacationRequest.objects.create(**self.vacation_request_user)
+ VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.get(reverse("vacation-list"))
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 2)
+
+ def test_vacation_list_manager(self):
+ """Test listing all vacations endpoint for a manager."""
+ self.user.job_position.rank = 5
+ self.user.job_position.save()
+ self.test_user.area.manager = self.user
+ self.test_user.area.save()
+ VacationRequest.objects.create(**self.vacation_request_user)
+ VacationRequest.objects.create(**self.vacation_request_user)
+ # Change the area of the test user to match the user's area
+ demo_user_admin = self.create_demo_user_admin()
+ demo_user_admin.area = self.user.area
+ demo_user_admin.save()
+ demo_user_admin.job_position.rank = 1
+ demo_user_admin.job_position.save()
+ VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.get(reverse("vacation-list"))
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 3, response.data)
+
+ def test_vacation_list_manager_multiple_areas(self):
+ """Test listing all vacations endpoint for a manager with multiple areas."""
+ self.test_user.area.manager = self.user
+ self.test_user.area.save()
+ self.user.area = Area.objects.create(name="Test Area 2", manager=self.user)
+ self.user.save()
+ # Check that the user has a different area than the manager
+ self.assertNotEqual(self.test_user.area, self.user.area)
+ VacationRequest.objects.create(**self.vacation_request_user)
+ self.create_demo_user()
+ Area.objects.create(name="Test Area", manager=self.user)
+ VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.get(reverse("vacation-list"))
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 2)
+
+ def test_vacation_list_payroll(self):
+ """Test listing all vacations endpoint for payroll."""
+ self.user.user_permissions.add(self.permission)
+ VacationRequest.objects.create(**self.vacation_request_user)
+ VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.get(reverse("vacation-list"))
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 2)
+
+ def test_vacation_list_vacation_manager(self):
+ """Test listing all vacations endpoint for a vacation manager."""
+ self.test_user.area.vacation_managers.add(self.user)
+ print(self.test_user.area)
+ print(self.user.area)
+ VacationRequest.objects.create(**self.vacation_request_user)
+ VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.get(reverse("vacation-list"))
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 2)
+
+ def test_vacation_list_hr(self):
+ """Test listing all vacations endpoint for HR."""
+ self.user.job_position.name = "GERENTE DE GESTION HUMANA"
+ self.user.job_position.save()
+ VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.get(reverse("vacation-list"))
+ vacation_requests = VacationRequest.objects.all()
+ serializer = VacationRequestSerializer(vacation_requests, many=True)
+ self.assertEqual(response.data, serializer.data)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_vacation_retrieve(self):
+ """Test retrieving a vacation endpoint."""
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.get(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk})
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(
+ response.data["start_date"], self.vacation_request["start_date"]
+ )
+ self.assertEqual(response.data["end_date"], self.vacation_request["end_date"])
+
+ def test_vacation_create_end_before_start(self):
+ """Test creating a vacation with the end date before the start date."""
+ self.vacation_request["end_date"] = "2021-01-04"
+ self.vacation_request["sat_is_working"] = False
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.data["non_field_errors"][0],
+ "La fecha de inicio no puede ser mayor a la fecha de fin.",
+ )
+
+ def test_vacation_owner_cancel_approved(self):
+ """Test the owner cancelling an approved vacation."""
+ self.vacation_request_user["boss_is_approved"] = True
+ self.vacation_request_user["user"] = self.user
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"status": "CANCELADA"},
+ )
+ self.assertEqual(
+ response.status_code, status.HTTP_400_BAD_REQUEST, response.data
+ )
+ self.assertEqual(
+ str(response.data["non_field_errors"][0]),
+ "No puedes cancelar una solicitud que ya ha recibido aprobación.",
+ )
+
+ def test_vacation_cancel_no_owner(self):
+ """Test cancelling a vacation without being the owner."""
+ self.vacation_request_user["user"] = self.test_user
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"status": "CANCELADA"},
+ )
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
+
+ def test_vacation_boss_approve(self):
+ """Test the boss approving a vacation."""
+ self.user.job_position.rank = 2
+ self.user.job_position.save()
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"boss_is_approved": True},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
+ self.assertTrue(response.data["boss_is_approved"])
+ vacation_object.refresh_from_db()
+ self.assertIsNotNone(vacation_object.boss_approved_at)
+
+ def test_vacation_manager_approve(self):
+ """Test the manager approving a vacation."""
+ self.user.job_position.rank = 5
+ self.user.job_position.save()
+ self.test_user.job_position.name = "GERENTE DE GESTION HUMANA"
+ self.test_user.job_position.save()
+ self.vacation_request_user["boss_is_approved"] = True
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"manager_is_approved": True},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
+ self.assertTrue(response.data["manager_is_approved"])
+ vacation_object.refresh_from_db()
+ self.assertIsNotNone(vacation_object.manager_approved_at)
+
+ def test_vacation_manager_reject(self):
+ """Test the manager rejecting a vacation."""
+ self.user.job_position.rank = 5
+ self.user.job_position.save()
+ self.vacation_request_user["boss_is_approved"] = True
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"manager_is_approved": False},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
+ self.assertFalse(response.data["manager_is_approved"])
+ self.assertEqual(response.data["status"], "RECHAZADA")
+ vacation_object.refresh_from_db()
+ self.assertIsNotNone(vacation_object.manager_approved_at)
+
+ def test_vacation_manager_approve_before_boss(self):
+ """Test the manager approving a vacation before the boss."""
+ self.user.job_position.rank = 5
+ self.user.job_position.save()
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"manager_is_approved": True},
+ )
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
+
+ def test_vacation_manager_approve_no_manager(self):
+ """Test the manager approving a vacation without being a manager."""
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"manager_is_approved": True},
+ )
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
+
+ def test_vacation_hr_approve(self):
+ """Test HR approving a vacation."""
+ self.user.job_position.name = "GERENTE DE GESTION HUMANA"
+ self.user.job_position.save()
+ test_user = self.create_demo_user()
+ test_user.user_permissions.add(self.permission)
+ self.vacation_request_user["manager_is_approved"] = True
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"hr_is_approved": True},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
+ self.assertTrue(response.data["hr_is_approved"])
+ vacation_object.refresh_from_db()
+ self.assertIsNotNone(vacation_object.hr_approved_at)
+
+ def test_vacation_hr_reject(self):
+ """Test HR rejecting a vacation."""
+ self.user.job_position.name = "GERENTE DE GESTION HUMANA"
+ self.user.job_position.save()
+ self.vacation_request_user["manager_is_approved"] = True
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"hr_is_approved": False},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
+ self.assertFalse(response.data["hr_is_approved"])
+ self.assertEqual(response.data["status"], "RECHAZADA")
+ vacation_object.refresh_from_db()
+ self.assertIsNotNone(vacation_object.hr_approved_at)
+
+ def test_vacation_hr_approve_before_manager(self):
+ """Test HR approving a vacation before the manager."""
+ self.user.job_position.name = "GERENTE DE GESTION HUMANA"
+ self.user.job_position.save()
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"hr_is_approved": True},
+ )
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
+
+ def test_vacation_hr_approve_no_hr(self):
+ """Test HR approving a vacation without being an HR."""
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"hr_is_approved": True},
+ )
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
+
+ def test_vacation_payroll_approve(self):
+ """Test payroll approving a vacation."""
+ self.user.user_permissions.add(self.permission)
+ self.vacation_request_user["hr_is_approved"] = True
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"payroll_is_approved": True},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
+ self.assertTrue(response.data["payroll_is_approved"])
+ vacation_object.refresh_from_db()
+ self.assertIsNotNone(vacation_object.payroll_approved_at)
+
+ def test_vacation_payroll_reject(self):
+ """Test payroll rejecting a vacation."""
+ self.user.user_permissions.add(self.permission)
+ self.vacation_request_user["hr_is_approved"] = True
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"payroll_is_approved": False},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
+ self.assertFalse(response.data["payroll_is_approved"])
+ self.assertEqual(response.data["status"], "RECHAZADA")
+ vacation_object.refresh_from_db()
+ self.assertIsNotNone(vacation_object.payroll_approved_at)
+
+ def test_vacation_payroll_approve_before_hr(self):
+ """Test payroll approving a vacation before HR."""
+ self.user.user_permissions.add(self.permission)
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"payroll_is_approved": True},
+ )
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
+
+ def test_vacation_payroll_approve_no_payroll(self):
+ """Test payroll approving a vacation without being in payroll."""
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.patch(
+ reverse("vacation-detail", kwargs={"pk": vacation_object.pk}),
+ {"payroll_is_approved": True},
+ )
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
+
+ @freeze_time("2024-07-21 10:00:00")
+ def test_validate_vacation_request_after_20th(self):
+ """Test the validation of a vacation request after the 20th."""
+ super().setUp()
+ self.vacation_request["sat_is_working"] = False
+ self.vacation_request["start_date"] = "2024-08-12"
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(
+ response.status_code, status.HTTP_400_BAD_REQUEST, response.data
+ )
+ self.assertEqual(
+ response.data["non_field_errors"][0],
+ "Después del día 20 no puedes solicitar vacaciones para el mes siguiente.",
+ )
+
+ def test_validate_vacation_request_not_working_day(self):
+ """Test the validation of a vacation request on a non-working day."""
+ self.vacation_request["sat_is_working"] = False
+ self.vacation_request["start_date"] = "2024-01-01"
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(
+ response.status_code, status.HTTP_400_BAD_REQUEST, response.data
+ )
+ self.assertEqual(
+ response.data["non_field_errors"][0],
+ "No puedes iniciar tus vacaciones un día no laboral.",
+ )
+
+ def test_validate_vacation_request_not_working_day_sat(self):
+ """Test the validation of a vacation request on a Saturday."""
+ self.vacation_request["sat_is_working"] = False
+ self.vacation_request["start_date"] = "2024-05-04"
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(
+ response.status_code, status.HTTP_400_BAD_REQUEST, response.data
+ )
+ # This is fine because if the user doesn't work on Saturdays, they can't start their vacation on a Saturday
+ self.assertEqual(
+ response.data["non_field_errors"][0],
+ "No puedes iniciar tus vacaciones un día no laboral.",
+ )
+
+ def test_validate_vacation_request_not_working_day_sat_working(self):
+ """Test the validation of a vacation request on a Saturday with working Saturdays."""
+ self.vacation_request["sat_is_working"] = True
+ self.vacation_request["start_date"] = "2024-05-04"
+ self.vacation_request["end_date"] = "2024-05-06"
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)
+
+ def test_validate_vacation_request_not_working_day_end(self):
+ """Test the validation of a vacation request on a non-working day."""
+ self.vacation_request["sat_is_working"] = False
+ self.vacation_request["end_date"] = "2024-01-01"
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(
+ response.status_code, status.HTTP_400_BAD_REQUEST, response.data
+ )
+ self.assertEqual(
+ response.data["non_field_errors"][0],
+ "No puedes terminar tus vacaciones un día no laboral.",
+ )
+
+ def test_validate_vacation_request_not_working_day_sat_end(self):
+ """Test the validation of a vacation request on a Saturday."""
+ self.vacation_request["sat_is_working"] = False
+ self.vacation_request["end_date"] = "2024-05-04"
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(
+ response.status_code, status.HTTP_400_BAD_REQUEST, response.data
+ )
+ # This is fine because if the user doesn't work on Saturdays, they can't end their vacation on a Saturday
+ self.assertEqual(
+ response.data["non_field_errors"][0],
+ "No puedes terminar tus vacaciones un día no laboral.",
+ )
+
+ def test_validate_vacation_request_not_working_day_sat_working_end(self):
+ """Test the validation of a vacation request on a Saturday with working Saturdays."""
+ self.vacation_request["sat_is_working"] = True
+ self.vacation_request["start_date"] = "2024-05-03"
+ self.vacation_request["end_date"] = "2024-05-04"
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)
+
+ def test_validate_vacation_request_more_than_15_days(self):
+ """Test the validation of a vacation request with more than 15 days."""
+ self.vacation_request["sat_is_working"] = False
+ self.vacation_request["end_date"] = "2024-01-24"
+ response = self.client.post(
+ reverse("vacation-list"),
+ self.vacation_request,
+ )
+ self.assertEqual(
+ response.status_code, status.HTTP_400_BAD_REQUEST, response.data
+ )
+ self.assertEqual(
+ response.data["non_field_errors"][0],
+ "No puedes solicitar más de 15 días de vacaciones.",
+ )
+
+ def test_get_vacation_request(self):
+ """Test getting the vacation request PDF."""
+ self.user.user_permissions.add(self.permission)
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.get(
+ reverse("vacation-get-request", kwargs={"pk": vacation_object.pk})
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response["Content-Type"], "application/pdf")
+
+ def test_get_vacation_request_no_permission(self):
+ """Test getting the vacation request PDF without permission."""
+ self.user.job_position.rank = 1
+ self.user.job_position.save()
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.get(
+ reverse("vacation-get-request", kwargs={"pk": vacation_object.pk})
+ )
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_get_vacation_request_manager(self):
+ """Test getting the vacation request PDF as a manager."""
+ self.user.job_position.rank = 5
+ self.user.job_position.save()
+ self.test_user.area.manager = self.user
+ self.test_user.area.save()
+ vacation_object = VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.get(
+ reverse("vacation-get-request", kwargs={"pk": vacation_object.pk})
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response["Content-Type"], "application/pdf")
+
+ def test_get_manage_multiple_children(self):
+ """Test managing multiple children."""
+ self.test_user.area.parent = self.user.area
+ self.test_user.area.save()
+ VacationRequest.objects.create(**self.vacation_request_user)
+ response = self.client.get(
+ reverse("vacation-list"),
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
diff --git a/INSIGHTSAPI/vacation/views.py b/INSIGHTSAPI/vacation/views.py
index 6c048f7f..51ef4e70 100644
--- a/INSIGHTSAPI/vacation/views.py
+++ b/INSIGHTSAPI/vacation/views.py
@@ -1,433 +1,446 @@
-import base64
-import datetime
-
-import pdfkit
-from django.conf import settings
-from django.contrib.auth.models import AnonymousUser
-from django.core.mail import mail_admins, send_mail
-from django.db.models import Q
-from django.http import HttpResponse
-from django.template.loader import render_to_string
-from django.utils import timezone
-from rest_framework import status, viewsets
-from rest_framework.decorators import action
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-
-from notifications.utils import create_notification
-from users.models import User
-
-from .models import VacationRequest
-from .serializers import VacationRequestSerializer
-
-
-class VacationRequestViewSet(viewsets.ModelViewSet):
- queryset = VacationRequest.objects.all().select_related("user").order_by("-pk")
- serializer_class = VacationRequestSerializer
- permission_classes = [IsAuthenticated]
-
- def get_queryset(self):
- # Restrict queryset based on user permissions
- user = self.request.user
-
- # If the user is a manager of HR
- if user.job_position.name == "GERENTE DE GESTION HUMANA":
- return self.queryset.all()
-
- # If the user has payroll approval permissions
- elif user.has_perm("vacation.payroll_approval"):
- return self.queryset.all()
-
- # If the user has a management position with rank >= 2
- elif user.job_position.rank >= 2:
- children = user.area.get_children()
-
- # Check if the user is a manager of their area
- if children and user.area.manager == user:
- return self.queryset.filter(
- Q(user=user)
- | Q(user__area__manager=user)
- | Q(user__area__in=children)
- | (
- Q(user__job_position__rank__lt=user.job_position.rank)
- & Q(user__area=user.area)
- )
- )
- else:
- return self.queryset.filter(
- Q(user=user)
- | Q(user__area__manager=user)
- | (
- Q(user__job_position__rank__lt=user.job_position.rank)
- & Q(user__area=user.area)
- )
- )
-
- # If the user is a regular employee
- return self.queryset.filter(user=user)
-
- def create(self, request, *args, **kwargs):
- response = super().create(request, *args, **kwargs)
- if response.status_code == status.HTTP_201_CREATED and response.data:
- user_request = VacationRequest.objects.get(pk=response.data["id"]).user
- create_notification(
- "Solicitud de vacaciones creada",
- f"Se ha creado una solicitud de vacaciones a tu nombre del {response.data['start_date']} al {response.data['end_date']}.",
- user_request,
- )
- if user_request.area.manager:
- create_notification(
- "Nueva solicitud de vacaciones",
- f"Se ha creado una nueva solicitud de vacaciones para {user_request.get_full_name()} del {response.data['start_date']} al {response.data['end_date']}.",
- user_request.area.manager,
- )
- if user_request.area.manager.company_email:
- send_mail(
- "Nueva solicitud de vacaciones",
- f"Se ha creado una nueva solicitud de vacaciones para {user_request.get_full_name()} del {response.data['start_date']} al {response.data['end_date']}. Por favor revisa la solicitud en la intranet.",
- None,
- [str(user_request.area.manager.company_email)],
- )
- email_message = f"""
- Hola {user_request.get_full_name()},
-
- Nos complace informarte que se ha creado una solicitud de vacaciones a tu nombre para las fechas del {datetime.datetime.strptime(response.data['start_date'], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data['end_date'], "%Y-%m-%d").strftime("%d de %B del %Y")}.
-
- Información Adicional:
- 1. Aprobación Pendiente: Tu solicitud está pendiente de aprobación. Recibirás una notificación por correo electrónico una vez que tu solicitud sea aprobada o rechazada.
- 2. Política de Vacaciones: Recuerda que es tu responsabilidad familiarizarte con nuestra política de vacaciones. Puedes encontrar el documento completo en la intranet sección "Gestión documental" -> "POLÍTICA DISFRUTE DE VACACIONES".
- 3. Planificación de Proyectos: Si tienes proyectos pendientes o tareas que necesitan seguimiento durante tu ausencia, por favor coordina con tu equipo para asegurar una transición sin problemas.
-
- Si tienes alguna pregunta o necesitas asistencia adicional, no dudes en ponerte en contacto con la Gerencia de Recursos Humanos.
-
- ¡Esperamos que tu solicitud sea aprobada y que disfrutes de unas vacaciones relajantes! ⛱
- """
- html_message = f"""
-
-
-
-
-
Hola {user_request.get_full_name()},
-
Nos complace informarte que se ha creado una solicitud de vacaciones a tu nombre para las fechas del {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}.
-
Información Adicional
-
-
Aprobación Pendiente: Tu solicitud está pendiente de aprobación. Recibirás una notificación por correo electrónico una vez que tu solicitud sea aprobada o rechazada.
-
Política de Vacaciones: Recuerda que es tu responsabilidad familiarizarte con nuestra política de vacaciones. Puedes encontrar el documento completo en la intranet sección "Gestión documental" -> "POLÍTICA DISFRUTE DE VACACIONES".
-
Planificación de Proyectos: Si tienes proyectos pendientes o tareas que necesitan seguimiento durante tu ausencia, por favor coordina con tu equipo para asegurar una transición sin problemas.
-
-
Si tienes alguna pregunta o necesitas asistencia adicional, no dudes en ponerte en contacto con la Gerencia de Recursos Humanos.
-
¡Esperamos que tu solicitud sea aprobada y que disfrutes de unas vacaciones relajantes! ⛱
-
-
- """
- send_mail(
- "Solicitud de vacaciones",
- email_message,
- None,
- [str(user_request.email)],
- html_message=html_message,
- )
- return response
-
- def partial_update(self, request, *args, **kwargs):
-
- if isinstance(request.user, AnonymousUser):
- return Response(
- {"detail": "Authentication credentials were not provided."},
- status=status.HTTP_403_FORBIDDEN,
- )
- # Check if the user is updating the hr_is_approved field
- if "boss_is_approved" in request.data:
- # Check if the user is a manager
- if request.user.job_position.rank >= 2:
- if self.get_object().boss_is_approved is not None:
- return Response(
- {"detail": "No puedes modificar esta solicitud."},
- status=status.HTTP_400_BAD_REQUEST,
- )
- response = super().partial_update(request, *args, **kwargs)
- if (
- response.status_code == status.HTTP_200_OK
- and response.data
- and response.data["boss_is_approved"]
- ):
- manager_user = request.user.area.manager
- if not manager_user:
- mail_admins(
- f"No hay usuarios con el cargo de GERENTE para el área {request.user.area}",
- f"No hay usuarios con el cargo de GERENTE para el área {request.user.area}",
- )
- return response
- create_notification(
- "Una solicitud necesita tu aprobación",
- f"{request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data['username']}. Ahora necesita tu aprobación.",
- manager_user,
- )
- return response
-
- else:
- return Response(
- {"detail": f"You do not have permission to perform this action."},
- status=status.HTTP_403_FORBIDDEN,
- )
- elif "manager_is_approved" in request.data:
- # Check if the user is a manager
- if (
- request.user.job_position.rank >= 5
- and self.get_object().boss_is_approved
- ):
- if self.get_object().manager_is_approved is not None:
- return Response(
- {"detail": "No puedes modificar esta solicitud."},
- status=status.HTTP_400_BAD_REQUEST,
- )
- response = super().partial_update(request, *args, **kwargs)
- if (
- response.status_code == status.HTTP_200_OK
- and response.data
- and response.data["manager_is_approved"]
- ):
- hr_user = User.objects.filter(
- job_position__name="GERENTE DE GESTION HUMANA"
- ).first()
- if not hr_user:
- mail_admins(
- "No hay usuarios con el cargo de GERENTE DE GESTION HUMANA",
- "No hay usuarios con el cargo de GERENTE DE GESTION HUMANA",
- )
- return response
- create_notification(
- "Una solicitud necesita tu aprobación",
- f"{request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {self.get_object().user}. Ahora necesita tu aprobación.",
- hr_user,
- )
- hr_message = f"""
- Hola {hr_user.get_full_name()} 👋,
-
- {request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}.
-
- Ahora esta a la espera de tu aprobación. Por favor revisa la solicitud y apruébala si estas de acuerdo con las fechas solicitadas.
- """
- send_mail(
- "Solicitud de vacaciones aprobada por un gerente",
- hr_message,
- None,
- [str(hr_user.company_email)],
- )
- payroll_user = User.objects.filter(
- user_permissions__codename="payroll_approval"
- ).first()
- if not payroll_user:
- mail_admins(
- "No hay usuarios con el permiso de payroll_approval",
- "No hay usuarios con el permiso de payroll_approval",
- )
- return response
- create_notification(
- "Una solicitud de vacaciones ha sido aprobada por un gerente",
- f"La solicitud de vacaciones de {response.data['username']} ha sido aprobada por {request.user.get_full_name()}. Ahora sera revisada por la Gerencia de Recursos Humanos.",
- payroll_user,
- )
- payroll_message = f"""
- Hola {payroll_user.get_full_name()} 👋,
-
- {request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}.
-
- Ahora esta a la espera de la aprobación de la Gerencia de Recursos Humanos.
- """
- send_mail(
- "Una solicitud de vacaciones ha sido aprobada por un gerente",
- payroll_message,
- None,
- [str(payroll_user.company_email)],
- )
- return response
-
- else:
- return Response(
- {"detail": f"You do not have permission to perform this action."},
- status=status.HTTP_403_FORBIDDEN,
- )
- elif "hr_is_approved" in request.data:
- # Check if the user is an HR and that the manager has already approved the request
- if (
- request.user.job_position.name == "GERENTE DE GESTION HUMANA"
- and self.get_object().manager_is_approved
- ):
- if self.get_object().hr_is_approved is not None:
- return Response(
- {"detail": "No puedes modificar esta solicitud."},
- status=status.HTTP_400_BAD_REQUEST,
- )
- response = super().partial_update(request, *args, **kwargs)
- if (
- response.status_code == status.HTTP_200_OK
- and response.data
- and response.data["hr_is_approved"]
- ):
- payroll_user = User.objects.filter(
- user_permissions__codename="payroll_approval"
- ).first()
- if not payroll_user:
- mail_admins(
- "No hay usuarios con el permiso de payroll_approval",
- "No hay usuarios con el permiso de payroll_approval",
- )
- return response
- create_notification(
- "Una solicitud de vacaciones necesita tu aprobación",
- f"La Gerencia de Recursos Humanos ha aprobado la solicitud de vacaciones de {response.data['username']}. Ahora necesita tu aprobación.",
- payroll_user,
- )
- payroll_message = f"""
- Hola {payroll_user.get_full_name()} 👋,
-
- La Gerencia de Recursos Humanos ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}.
-
- Ahora esta a la espera de tu aprobación final. Por favor revisa la solicitud y apruébala si estas de acuerdo con las fechas solicitadas.
- """
- send_mail(
- "Solicitud de vacaciones en espera de tu aprobación",
- payroll_message,
- None,
- [str(payroll_user.company_email)],
- )
- return response
- else:
- return Response(
- {
- "detail": f"You do not have permission to perform this action {request.user.job_position.name}."
- },
- status=status.HTTP_403_FORBIDDEN,
- )
- elif "payroll_is_approved" in request.data:
- # Check if the user is in payroll and that the HR has already approved the request
- if (
- request.user.has_perm("vacation.payroll_approval")
- and self.get_object().hr_is_approved
- ):
- if self.get_object().payroll_is_approved is not None:
- return Response(
- {"detail": "No puedes modificar esta solicitud."},
- status=status.HTTP_400_BAD_REQUEST,
- )
- return super().partial_update(request, *args, **kwargs)
- if (
- "status" in request.data
- and request.user == self.get_object().user
- and request.data["status"] == "CANCELADA"
- ):
- return super().partial_update(request, *args, **kwargs)
-
- return Response(
- {"detail": "You do not have permission to perform this action."},
- status=status.HTTP_403_FORBIDDEN,
- )
-
- # Example link: http://localhost:8000/vacation/1/get-request/
- @action(
- detail=True, methods=["get"], url_path="get-request", url_name="get-request"
- )
- def generate_request(self, request, pk=None):
- context = {
- "vacation": self.get_object(),
- "current_date": timezone.now().strftime("%d de %B de %Y").capitalize(),
- "company_logo": base64.b64encode(
- open(str(settings.STATIC_ROOT) + "/images/just_logo.png", "rb").read()
- ).decode("utf-8"),
- }
- # Import the html template
- rendered_template = render_to_string(
- "vacation_request.html",
- context,
- )
- # PDF options
- options = {
- "page-size": "Letter",
- "orientation": "portrait",
- "encoding": "UTF-8",
- "margin-top": "0mm",
- "margin-right": "0mm",
- "margin-bottom": "0mm",
- "margin-left": "0mm",
- }
- pdf = pdfkit.from_string(rendered_template, False, options=options)
- response = HttpResponse(pdf, content_type="application/pdf")
- response["Content-Disposition"] = (
- 'inline; filename="Solicitud de vacaciones - {}.pdf"'.format(
- self.get_object().user.get_full_name()
- )
- )
- return response
-
- @action(
- detail=True, methods=["get"], url_path="get-response", url_name="get-response"
- )
- def generate_response(self, request, pk=None):
- context = {
- "vacation": self.get_object(),
- "current_date": timezone.now().strftime("%d de %B de %Y").capitalize(),
- "company_logo": base64.b64encode(
- open(str(settings.STATIC_ROOT) + "/images/just_logo.png", "rb").read()
- ).decode("utf-8"),
- "company_logo_vertical": base64.b64encode(
- open(
- str(settings.STATIC_ROOT) + "/images/vertical_logo.png", "rb"
- ).read()
- ).decode("utf-8"),
- }
- # Import the html template
- rendered_template = render_to_string(
- "vacation_response.html",
- context,
- )
- # PDF options
- options = {
- "page-size": "Letter",
- "orientation": "portrait",
- "encoding": "UTF-8",
- "margin-top": "0mm",
- "margin-right": "0mm",
- "margin-bottom": "0mm",
- "margin-left": "0mm",
- }
- pdf = pdfkit.from_string(rendered_template, False, options=options)
- response = HttpResponse(pdf, content_type="application/pdf")
- response["Content-Disposition"] = (
- 'inline; filename="Respuesta a solicitud de vacaciones - {}.pdf"'.format(
- self.get_object().user.get_full_name()
- )
- )
- return response
+import base64
+import datetime
+from typing import cast
+
+import pdfkit
+from django.conf import settings
+from django.contrib.auth.models import AnonymousUser
+from django.core.mail import mail_admins, send_mail
+from django.db.models import Q
+from django.http import HttpResponse
+from django.template.loader import render_to_string
+from django.utils import timezone
+from rest_framework import status, viewsets
+from rest_framework.decorators import action
+from rest_framework.response import Response
+
+from hierarchy.models import Area
+from notifications.utils import create_notification
+from users.models import User
+
+from .models import VacationRequest
+from .serializers import VacationRequestSerializer
+
+
+class VacationRequestViewSet(viewsets.ModelViewSet):
+ queryset = VacationRequest.objects.all().select_related("user").order_by("-pk")
+ serializer_class = VacationRequestSerializer
+
+ def get_queryset(self):
+ # Restrict queryset based on user permissions
+ user = cast(User, self.request.user)
+
+ if not self.queryset:
+ return self.queryset
+
+ # If the user is a manager of HR
+ if user.job_position.name == "GERENTE DE GESTION HUMANA":
+ return self.queryset.all()
+
+ # If the user has payroll approval permissions
+ elif user.has_perm("vacation.payroll_approval"):
+ return self.queryset.all()
+
+ elif Area.objects.filter(vacation_managers=user).exists():
+ return self.queryset.filter(
+ Q(user=user) # The user is the owner of the request
+ | Q(user__area__manager=user) # The user is the manager of the area
+ | (
+ Q(user__area__vacation_managers=user)
+ & Q(user__job_position__rank__lt=user.job_position.rank)
+ ) # The user is a vacation manager of the area
+ )
+
+ # If the user has a management position with rank >= 2
+ elif user.job_position.rank >= 2:
+ children = user.area.get_children()
+
+ # Check if the user is a manager of their area
+ if children and user.area.manager == user:
+ return self.queryset.filter(
+ Q(user=user)
+ | Q(user__area__manager=user)
+ | Q(user__area__in=children)
+ | (
+ Q(user__job_position__rank__lt=user.job_position.rank)
+ & Q(user__area=user.area)
+ )
+ )
+ else:
+ return self.queryset.filter(
+ Q(user=user)
+ | Q(user__area__manager=user)
+ | (
+ Q(user__job_position__rank__lt=user.job_position.rank)
+ & Q(user__area=user.area)
+ )
+ )
+
+ # If the user is a regular employee
+ return self.queryset.filter(user=user)
+
+ def create(self, request, *args, **kwargs):
+ response = super().create(request, *args, **kwargs)
+ if response.status_code == status.HTTP_201_CREATED and response.data:
+ user_request = VacationRequest.objects.get(pk=response.data["id"]).user
+ create_notification(
+ "Solicitud de vacaciones creada",
+ f"Se ha creado una solicitud de vacaciones a tu nombre del {response.data['start_date']} al {response.data['end_date']}.",
+ user_request,
+ )
+ if user_request.area.manager:
+ create_notification(
+ "Nueva solicitud de vacaciones",
+ f"Se ha creado una nueva solicitud de vacaciones para {user_request.get_full_name()} del {response.data['start_date']} al {response.data['end_date']}.",
+ user_request.area.manager,
+ )
+ if user_request.area.manager.company_email:
+ send_mail(
+ "Nueva solicitud de vacaciones",
+ f"Se ha creado una nueva solicitud de vacaciones para {user_request.get_full_name()} del {response.data['start_date']} al {response.data['end_date']}. Por favor revisa la solicitud en la intranet.",
+ None,
+ [str(user_request.area.manager.company_email)],
+ )
+ email_message = f"""
+ Hola {user_request.get_full_name()},
+
+ Nos complace informarte que se ha creado una solicitud de vacaciones a tu nombre para las fechas del {datetime.datetime.strptime(response.data['start_date'], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data['end_date'], "%Y-%m-%d").strftime("%d de %B del %Y")}.
+
+ Información Adicional:
+ 1. Aprobación Pendiente: Tu solicitud está pendiente de aprobación. Recibirás una notificación por correo electrónico una vez que tu solicitud sea aprobada o rechazada.
+ 2. Política de Vacaciones: Recuerda que es tu responsabilidad familiarizarte con nuestra política de vacaciones. Puedes encontrar el documento completo en la intranet sección "Gestión documental" -> "POLÍTICA DISFRUTE DE VACACIONES".
+ 3. Planificación de Proyectos: Si tienes proyectos pendientes o tareas que necesitan seguimiento durante tu ausencia, por favor coordina con tu equipo para asegurar una transición sin problemas.
+
+ Si tienes alguna pregunta o necesitas asistencia adicional, no dudes en ponerte en contacto con la Gerencia de Recursos Humanos.
+
+ ¡Esperamos que tu solicitud sea aprobada y que disfrutes de unas vacaciones relajantes! ⛱
+ """
+ html_message = f"""
+
+
+
+
+
Hola {user_request.get_full_name()},
+
Nos complace informarte que se ha creado una solicitud de vacaciones a tu nombre para las fechas del {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}.
+
Información Adicional
+
+
Aprobación Pendiente: Tu solicitud está pendiente de aprobación. Recibirás una notificación por correo electrónico una vez que tu solicitud sea aprobada o rechazada.
+
Política de Vacaciones: Recuerda que es tu responsabilidad familiarizarte con nuestra política de vacaciones. Puedes encontrar el documento completo en la intranet sección "Gestión documental" -> "POLÍTICA DISFRUTE DE VACACIONES".
+
Planificación de Proyectos: Si tienes proyectos pendientes o tareas que necesitan seguimiento durante tu ausencia, por favor coordina con tu equipo para asegurar una transición sin problemas.
+
+
Si tienes alguna pregunta o necesitas asistencia adicional, no dudes en ponerte en contacto con la Gerencia de Recursos Humanos.
+
¡Esperamos que tu solicitud sea aprobada y que disfrutes de unas vacaciones relajantes! ⛱
+
+
+ """
+ send_mail(
+ "Solicitud de vacaciones",
+ email_message,
+ None,
+ [str(user_request.email)],
+ html_message=html_message,
+ )
+ return response
+
+ def partial_update(self, request, *args, **kwargs):
+
+ if isinstance(request.user, AnonymousUser):
+ return Response(
+ {"detail": "Authentication credentials were not provided."},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ # Check if the user is updating the hr_is_approved field
+ if "boss_is_approved" in request.data:
+ # Check if the user is a manager
+ if request.user.job_position.rank >= 2:
+ if self.get_object().boss_is_approved is not None:
+ return Response(
+ {"detail": "No puedes modificar esta solicitud."},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+ response = super().partial_update(request, *args, **kwargs)
+ if (
+ response.status_code == status.HTTP_200_OK
+ and response.data
+ and response.data["boss_is_approved"]
+ ):
+ manager_user = request.user.area.manager
+ if not manager_user:
+ mail_admins(
+ f"No hay usuarios con el cargo de GERENTE para el área {request.user.area}",
+ f"No hay usuarios con el cargo de GERENTE para el área {request.user.area}",
+ )
+ return response
+ create_notification(
+ "Una solicitud necesita tu aprobación",
+ f"{request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data['username']}. Ahora necesita tu aprobación.",
+ manager_user,
+ )
+ return response
+
+ else:
+ return Response(
+ {"detail": f"You do not have permission to perform this action."},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ elif "manager_is_approved" in request.data:
+ # Check if the user is a manager
+ if (
+ request.user.job_position.rank >= 5
+ and self.get_object().boss_is_approved
+ ):
+ if self.get_object().manager_is_approved is not None:
+ return Response(
+ {"detail": "No puedes modificar esta solicitud."},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+ response = super().partial_update(request, *args, **kwargs)
+ if (
+ response.status_code == status.HTTP_200_OK
+ and response.data
+ and response.data["manager_is_approved"]
+ ):
+ hr_user = User.objects.filter(
+ job_position__name="GERENTE DE GESTION HUMANA"
+ ).first()
+ if not hr_user:
+ mail_admins(
+ "No hay usuarios con el cargo de GERENTE DE GESTION HUMANA",
+ "No hay usuarios con el cargo de GERENTE DE GESTION HUMANA",
+ )
+ return response
+ create_notification(
+ "Una solicitud necesita tu aprobación",
+ f"{request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {self.get_object().user}. Ahora necesita tu aprobación.",
+ hr_user,
+ )
+ hr_message = f"""
+ Hola {hr_user.get_full_name()} 👋,
+
+ {request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}.
+
+ Ahora esta a la espera de tu aprobación. Por favor revisa la solicitud y apruébala si estas de acuerdo con las fechas solicitadas.
+ """
+ send_mail(
+ "Solicitud de vacaciones aprobada por un gerente",
+ hr_message,
+ None,
+ [str(hr_user.company_email)],
+ )
+ payroll_user = User.objects.filter(
+ user_permissions__codename="payroll_approval"
+ ).first()
+ if not payroll_user:
+ mail_admins(
+ "No hay usuarios con el permiso de payroll_approval",
+ "No hay usuarios con el permiso de payroll_approval",
+ )
+ return response
+ create_notification(
+ "Una solicitud de vacaciones ha sido aprobada por un gerente",
+ f"La solicitud de vacaciones de {response.data['username']} ha sido aprobada por {request.user.get_full_name()}. Ahora sera revisada por la Gerencia de Recursos Humanos.",
+ payroll_user,
+ )
+ payroll_message = f"""
+ Hola {payroll_user.get_full_name()} 👋,
+
+ {request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}.
+
+ Ahora esta a la espera de la aprobación de la Gerencia de Recursos Humanos.
+ """
+ send_mail(
+ "Una solicitud de vacaciones ha sido aprobada por un gerente",
+ payroll_message,
+ None,
+ [str(payroll_user.company_email)],
+ )
+ return response
+
+ else:
+ return Response(
+ {"detail": f"You do not have permission to perform this action."},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ elif "hr_is_approved" in request.data:
+ # Check if the user is an HR and that the manager has already approved the request
+ if (
+ request.user.job_position.name == "GERENTE DE GESTION HUMANA"
+ and self.get_object().manager_is_approved
+ ):
+ if self.get_object().hr_is_approved is not None:
+ return Response(
+ {"detail": "No puedes modificar esta solicitud."},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+ response = super().partial_update(request, *args, **kwargs)
+ if (
+ response.status_code == status.HTTP_200_OK
+ and response.data
+ and response.data["hr_is_approved"]
+ ):
+ payroll_user = User.objects.filter(
+ user_permissions__codename="payroll_approval"
+ ).first()
+ if not payroll_user:
+ mail_admins(
+ "No hay usuarios con el permiso de payroll_approval",
+ "No hay usuarios con el permiso de payroll_approval",
+ )
+ return response
+ create_notification(
+ "Una solicitud de vacaciones necesita tu aprobación",
+ f"La Gerencia de Recursos Humanos ha aprobado la solicitud de vacaciones de {response.data['username']}. Ahora necesita tu aprobación.",
+ payroll_user,
+ )
+ payroll_message = f"""
+ Hola {payroll_user.get_full_name()} 👋,
+
+ La Gerencia de Recursos Humanos ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}.
+
+ Ahora esta a la espera de tu aprobación final. Por favor revisa la solicitud y apruébala si estas de acuerdo con las fechas solicitadas.
+ """
+ send_mail(
+ "Solicitud de vacaciones en espera de tu aprobación",
+ payroll_message,
+ None,
+ [str(payroll_user.company_email)],
+ )
+ return response
+ else:
+ return Response(
+ {
+ "detail": f"You do not have permission to perform this action {request.user.job_position.name}."
+ },
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ elif "payroll_is_approved" in request.data:
+ # Check if the user is in payroll and that the HR has already approved the request
+ if (
+ request.user.has_perm("vacation.payroll_approval")
+ and self.get_object().hr_is_approved
+ ):
+ if self.get_object().payroll_is_approved is not None:
+ return Response(
+ {"detail": "No puedes modificar esta solicitud."},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+ return super().partial_update(request, *args, **kwargs)
+ if (
+ "status" in request.data
+ and request.user == self.get_object().user
+ and request.data["status"] == "CANCELADA"
+ ):
+ return super().partial_update(request, *args, **kwargs)
+
+ return Response(
+ {"detail": "You do not have permission to perform this action."},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+
+ # Example link: http://localhost:8000/vacation/1/get-request/
+ @action(
+ detail=True, methods=["get"], url_path="get-request", url_name="get-request"
+ )
+ def generate_request(self, request, pk=None):
+ context = {
+ "vacation": self.get_object(),
+ "current_date": timezone.now().strftime("%d de %B de %Y").capitalize(),
+ "company_logo": base64.b64encode(
+ open(str(settings.STATIC_ROOT) + "/images/just_logo.png", "rb").read()
+ ).decode("utf-8"),
+ }
+ # Import the html template
+ rendered_template = render_to_string(
+ "vacation_request.html",
+ context,
+ )
+ # PDF options
+ options = {
+ "page-size": "Letter",
+ "orientation": "portrait",
+ "encoding": "UTF-8",
+ "margin-top": "0mm",
+ "margin-right": "0mm",
+ "margin-bottom": "0mm",
+ "margin-left": "0mm",
+ }
+ pdf = pdfkit.from_string(rendered_template, False, options=options)
+ response = HttpResponse(pdf, content_type="application/pdf")
+ response["Content-Disposition"] = (
+ 'inline; filename="Solicitud de vacaciones - {}.pdf"'.format(
+ self.get_object().user.get_full_name()
+ )
+ )
+ return response
+
+ @action(
+ detail=True, methods=["get"], url_path="get-response", url_name="get-response"
+ )
+ def generate_response(self, request, pk=None):
+ context = {
+ "vacation": self.get_object(),
+ "current_date": timezone.now().strftime("%d de %B de %Y").capitalize(),
+ "company_logo": base64.b64encode(
+ open(str(settings.STATIC_ROOT) + "/images/just_logo.png", "rb").read()
+ ).decode("utf-8"),
+ "company_logo_vertical": base64.b64encode(
+ open(
+ str(settings.STATIC_ROOT) + "/images/vertical_logo.png", "rb"
+ ).read()
+ ).decode("utf-8"),
+ }
+ # Import the html template
+ rendered_template = render_to_string(
+ "vacation_response.html",
+ context,
+ )
+ # PDF options
+ options = {
+ "page-size": "Letter",
+ "orientation": "portrait",
+ "encoding": "UTF-8",
+ "margin-top": "0mm",
+ "margin-right": "0mm",
+ "margin-bottom": "0mm",
+ "margin-left": "0mm",
+ }
+ pdf = pdfkit.from_string(rendered_template, False, options=options)
+ response = HttpResponse(pdf, content_type="application/pdf")
+ response["Content-Disposition"] = (
+ 'inline; filename="Respuesta a solicitud de vacaciones - {}.pdf"'.format(
+ self.get_object().user.get_full_name()
+ )
+ )
+ return response
diff --git a/frontend/.prettierignore b/frontend/.prettierignore
index 1eae0cf6..2003a155 100644
--- a/frontend/.prettierignore
+++ b/frontend/.prettierignore
@@ -1,2 +1,2 @@
-dist/
-node_modules/
+dist/
+node_modules/
diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json
new file mode 100644
index 00000000..4fd36104
--- /dev/null
+++ b/frontend/.vscode/launch.json
@@ -0,0 +1,15 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "chrome",
+ "request": "launch",
+ "name": "Launch Chrome against localhost",
+ "url": "http://172.16.0.115:8000",
+ "webRoot": "${workspaceFolder}"
+ }
+ ]
+}
diff --git a/frontend/src/components/pages/About.jsx b/frontend/src/components/pages/About.jsx
index c1524f11..96833700 100644
--- a/frontend/src/components/pages/About.jsx
+++ b/frontend/src/components/pages/About.jsx
@@ -13,6 +13,7 @@ import {
// Media
const generalManager = `${getApiUrl().apiUrl}static/images/managers/general-manager.webp`;
+const bidManager = `${getApiUrl().apiUrl}static/images/managers/bid-manager.webp`;
const rhManager = `${getApiUrl().apiUrl}static/images/managers/rh-manager.webp`;
const planningManager = `${getApiUrl().apiUrl}static/images/managers/planning-manager.webp`;
const collectionsSalesOperationsManager = `${getApiUrl().apiUrl}static/images/managers/collections-sales-operations-manager.webp`;
@@ -49,6 +50,13 @@ const managements = [
description:
'Garantizar la sostenibilidad de la compañía a través de la planeación, liderazgo y control de las diferentes áreas que permitan alcanzar los objetivos establecidos con los clientes, el recurso humano y los accionistas.',
},
+ {
+ name: 'Leidy Castillo',
+ management: 'Gerente de Licitaciones',
+ image: bidManager,
+ description:
+ 'Desarrollar y ejecutar estrategias de licitación altamente competitivas y alineadas con los objetivos organizacionales, optimizando los procesos y recursos para asegurar la participación exitosa en proyectos clave. Impulsar la mejora continua en la gestión de riesgos, el cumplimiento de normativas y la calidad de las propuestas, con el fin de fortalecer nuestra posición en el mercado, maximizar las oportunidades de negocio y consolidar relaciones de confianza con nuestros clientes y aliados.',
+ },
{
name: 'Diego González',
management: 'Gerente de Legal',
diff --git a/frontend/src/components/pages/Home.jsx b/frontend/src/components/pages/Home.jsx
index f8a61da8..898ccc50 100644
--- a/frontend/src/components/pages/Home.jsx
+++ b/frontend/src/components/pages/Home.jsx
@@ -5,6 +5,7 @@ const CarouselComponent = lazy(() => import('@components/shared/Carousel'));
import { EmblaCarousel } from '../shared/embla-carousel/EmblaCarousel';
import { getApiUrl } from '@assets/getApi.js';
import { handleError } from '@assets/handleError';
+const BirthdaySlider = lazy(() => import('@components/shared/BirthdaySlider'));
// Custom Hooks
import { useSnackbar } from '@contexts/SnackbarContext.jsx';
@@ -186,6 +187,8 @@ const Home = () => {
+
+
{
- useEffect(() => {
- const current = ref.current;
-
- if (current && listener) {
- current.addEventListener(event, listener);
- return () => current.removeEventListener(event, listener);
- }
- }, [ref, event, listener]);
-};
-
-const useProperty = (ref, prop, value) => {
- useEffect(() => {
- if (ref.current) {
- ref.current[prop] = value;
- }
- }, [ref, prop, value]);
-};
-
-export const CalendarMonth = forwardRef(
- function CalendarMonth(props, forwardedRef) {
- return ;
- }
-);
-
-export const CalendarRange = forwardRef(function CalendarRange(
- { onChange, showOutsideDays, isDateDisallowed, ...props },
- forwardedRef
-) {
- const ref = useRef();
- useImperativeHandle(forwardedRef, () => ref.current, []);
- useListener(ref, 'change', onChange);
- useProperty(ref, 'isDateDisallowed', isDateDisallowed);
-
- const today = new Date();
- const currentYear = today.getFullYear();
- const currentMonth = today.getMonth();
-
- let minDate;
- if (today.getDate() > 20) {
- minDate = new Date(currentYear, currentMonth + 2, 1);
- } else {
- minDate = new Date(currentYear, currentMonth + 1, 1);
- }
-
- // Assuming your calendar-range component accepts a min attribute for the minimum date
- return (
-
- );
-});
-
-const Picker = ({ value, onChange, isMondayToFriday, holidays }) => {
- const isDateDisallowed = (date) => {
- if (
- holidays
- .map((holiday) => holiday[0])
- .includes(date.toISOString().split('T')[0]) ||
- date.getDay() === 6 ||
- (isMondayToFriday ? date.getDay() === 5 : null)
- ) {
- return true;
- }
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-const VacationsRequest = ({ openVacation, setOpenVacation, getVacations }) => {
- const { showSnack } = useSnackbar();
- const [value, setValue] = useState('');
- const [textDate, setTextDate] = useState('');
- const [daysAmount, setDaysAmount] = useState('');
- const [collapseDate, setCollapseDate] = useState(true);
-
- const { isProgressVisible, showProgressbar, hideProgressbar } =
- useProgressbar();
-
- const [isMondayToFriday, setIsMondayToFriday] = useState(false);
- const [openCalendar, setOpenCalendar] = useState(false);
- const [holidays, setHolidays] = useState([]);
-
- const getTextMonth = (month) => {
- return new Date(
- new Date().setMonth(new Date().getMonth() + month)
- ).toLocaleString('es-ES', { month: 'long' });
- };
-
- const handleSchedule = (event) => {
- setIsMondayToFriday(event.target.value);
- setOpenCalendar(true);
- if (value !== '') {
- checkAmountOfDays({ target: { value: value } }, event.target.value);
- }
- };
-
- const getHolidays = async () => {
- const date = new Date();
- try {
- const response = await fetch(
- `${getApiUrl().apiUrl}services/holidays/${date.getFullYear()}/`,
- {
- method: 'GET',
- credentials: 'include',
- }
- );
-
- await handleError(response, showSnack);
-
- if (response.status === 200) {
- const data = await response.json();
- setHolidays(data);
- }
- } catch (error) {
- if (getApiUrl().environment === 'development') {
- console.error(error);
- }
- }
- };
-
- useEffect(() => {
- getHolidays();
- }, []);
-
- const checkAmountOfDays = (
- event,
- isMondayToFridayProp = isMondayToFriday
- ) => {
- const [startDate, endDate] = event.target.value.split('/');
-
- const start = new Date(startDate);
- const end = new Date(endDate);
-
- let diffDays = 0;
- for (let date = start; date <= end; date.setDate(date.getDate() + 1)) {
- const dayOfWeek = date.getDay();
- // exclude from the count the Sundays and the holidays and the Saturdays if the employee works from Monday to Friday
- if (
- dayOfWeek !== 6 &&
- !holidays
- .map((holiday) => holiday[0])
- .includes(date.toISOString().split('T')[0]) &&
- (dayOfWeek !== 5 || !isMondayToFridayProp)
- ) {
- diffDays++;
- }
- }
-
- setDaysAmount(diffDays);
- setTextDate(`${startDate} al ${endDate}`);
- setValue(event.target.value);
- if (diffDays > 15) {
- showSnack(
- 'error',
- 'El periodo de vacaciones seleccionado excede los 15 días hábiles permitidos.'
- );
- }
- };
-
- const onChange = (event) => {
- if (textDate === '') {
- checkAmountOfDays(event);
- return;
- }
-
- setCollapseDate(false);
- setTimeout(() => {
- checkAmountOfDays(event);
- }, 200);
-
- setTimeout(() => {
- setCollapseDate(true);
- }, 200);
- };
-
- const handleCloseVacationDialog = () => {
- setOpenVacation(false);
- setOpenCalendar(false);
- setTextDate('');
- setValue('');
- };
-
- const handleSubmitVacationRequest = async (event) => {
- event.preventDefault();
- showProgressbar();
- const formData = new FormData();
- formData.append('sat_is_working', !isMondayToFriday);
- formData.append('start_date', value.split('/')[0]);
- formData.append('end_date', value.split('/')[1]);
-
- try {
- const response = await fetch(`${getApiUrl().apiUrl}vacation/`, {
- method: 'POST',
- credentials: 'include',
- body: formData,
- });
-
- await handleError(response, showSnack);
-
- if (response.status === 201) {
- handleCloseVacationDialog();
- getVacations();
- showSnack(
- 'success',
- 'Solicitud de vacaciones enviada correctamente.'
- );
- }
- } catch (error) {
- if (getApiUrl().environment === 'development') {
- console.error(error);
- }
- } finally {
- hideProgressbar();
- }
- };
-
- return (
-
- );
-};
-
-export default VacationsRequest;
+import {
+ useEffect,
+ useRef,
+ forwardRef,
+ useImperativeHandle,
+ useState,
+} from 'react';
+
+//Libraries
+import 'cally';
+
+//Material UI
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Button,
+ Box,
+ Typography,
+ Collapse,
+ IconButton,
+ TextField,
+ MenuItem,
+} from '@mui/material';
+
+// MUI Lab
+import { LoadingButton } from '@mui/lab';
+
+// Custom Hooks
+import { useSnackbar } from '@contexts/SnackbarContext';
+import { useProgressbar } from '@contexts/ProgressbarContext';
+
+// Custom Components
+import { getApiUrl } from '@assets/getApi';
+import { handleError } from '@assets/handleError';
+
+const useListener = (ref, event, listener) => {
+ useEffect(() => {
+ const current = ref.current;
+
+ if (current && listener) {
+ current.addEventListener(event, listener);
+ return () => current.removeEventListener(event, listener);
+ }
+ }, [ref, event, listener]);
+};
+
+const useProperty = (ref, prop, value) => {
+ useEffect(() => {
+ if (ref.current) {
+ ref.current[prop] = value;
+ }
+ }, [ref, prop, value]);
+};
+
+export const CalendarMonth = forwardRef(
+ function CalendarMonth(props, forwardedRef) {
+ return ;
+ }
+);
+
+export const CalendarRange = forwardRef(function CalendarRange(
+ { onChange, showOutsideDays, isDateDisallowed, ...props },
+ forwardedRef
+) {
+ const ref = useRef();
+ useImperativeHandle(forwardedRef, () => ref.current, []);
+ useListener(ref, 'change', onChange);
+ useProperty(ref, 'isDateDisallowed', isDateDisallowed);
+
+ const today = new Date();
+ const currentYear = today.getFullYear();
+ const currentMonth = today.getMonth();
+
+ let minDate;
+ if (today.getDate() > 20) {
+ minDate = new Date(currentYear, currentMonth + 2, 1);
+ } else {
+ minDate = new Date(currentYear, currentMonth + 1, 1);
+ }
+
+ // Assuming your calendar-range component accepts a min attribute for the minimum date
+ return (
+
+ );
+});
+
+const Picker = ({ value, onChange, isMondayToFriday, holidays }) => {
+ const isDateDisallowed = (date) => {
+ if (
+ holidays
+ .map((holiday) => holiday[0])
+ .includes(date.toISOString().split('T')[0]) ||
+ date.getDay() === 6 ||
+ (isMondayToFriday ? date.getDay() === 5 : null)
+ ) {
+ return true;
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const VacationsRequest = ({ openVacation, setOpenVacation, getVacations }) => {
+ const { showSnack } = useSnackbar();
+ const [value, setValue] = useState('');
+ const [textDate, setTextDate] = useState('');
+ const [daysAmount, setDaysAmount] = useState('');
+ const [collapseDate, setCollapseDate] = useState(true);
+
+ const { isProgressVisible, showProgressbar, hideProgressbar } =
+ useProgressbar();
+
+ const [isMondayToFriday, setIsMondayToFriday] = useState(false);
+ const [openCalendar, setOpenCalendar] = useState(false);
+ const [holidays, setHolidays] = useState([]);
+
+ const getTextMonth = (month) => {
+ return new Date(
+ new Date().setMonth(new Date().getMonth() + month)
+ ).toLocaleString('es-ES', { month: 'long' });
+ };
+
+ const handleSchedule = (event) => {
+ setIsMondayToFriday(event.target.value);
+ setOpenCalendar(true);
+ if (value !== '') {
+ checkAmountOfDays({ target: { value: value } }, event.target.value);
+ }
+ };
+
+ const getHolidays = async () => {
+ const date = new Date();
+ try {
+ const response = await fetch(
+ `${getApiUrl().apiUrl}services/holidays/${date.getFullYear()}/`,
+ {
+ method: 'GET',
+ credentials: 'include',
+ }
+ );
+
+ await handleError(response, showSnack);
+
+ if (response.status === 200) {
+ const data = await response.json();
+ setHolidays(data);
+ }
+ } catch (error) {
+ if (getApiUrl().environment === 'development') {
+ console.error(error);
+ }
+ }
+ };
+
+ useEffect(() => {
+ getHolidays();
+ }, []);
+
+ const checkAmountOfDays = (
+ event,
+ isMondayToFridayProp = isMondayToFriday
+ ) => {
+ const [startDate, endDate] = event.target.value.split('/');
+
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+
+ let diffDays = 0;
+ for (let date = start; date <= end; date.setDate(date.getDate() + 1)) {
+ const dayOfWeek = date.getDay();
+ // exclude from the count the Sundays and the holidays and the Saturdays if the employee works from Monday to Friday
+ if (
+ dayOfWeek !== 6 &&
+ !holidays
+ .map((holiday) => holiday[0])
+ .includes(date.toISOString().split('T')[0]) &&
+ (dayOfWeek !== 5 || !isMondayToFridayProp)
+ ) {
+ diffDays++;
+ }
+ }
+
+ setDaysAmount(diffDays);
+ setTextDate(`${startDate} al ${endDate}`);
+ setValue(event.target.value);
+ if (diffDays > 15) {
+ showSnack(
+ 'error',
+ 'El periodo de vacaciones seleccionado excede los 15 días hábiles permitidos.'
+ );
+ }
+ };
+
+ const onChange = (event) => {
+ if (textDate === '') {
+ checkAmountOfDays(event);
+ return;
+ }
+
+ setCollapseDate(false);
+ setTimeout(() => {
+ checkAmountOfDays(event);
+ }, 200);
+
+ setTimeout(() => {
+ setCollapseDate(true);
+ }, 200);
+ };
+
+ const handleCloseVacationDialog = () => {
+ setOpenVacation(false);
+ setOpenCalendar(false);
+ setTextDate('');
+ setValue('');
+ };
+
+ const handleSubmitVacationRequest = async (event) => {
+ event.preventDefault();
+ showProgressbar();
+ const formData = new FormData();
+ formData.append('sat_is_working', !isMondayToFriday);
+ formData.append('start_date', value.split('/')[0]);
+ formData.append('end_date', value.split('/')[1]);
+
+ try {
+ const response = await fetch(`${getApiUrl().apiUrl}vacation/`, {
+ method: 'POST',
+ credentials: 'include',
+ body: formData,
+ });
+
+ await handleError(response, showSnack);
+
+ if (response.status === 201) {
+ handleCloseVacationDialog();
+ getVacations();
+ showSnack(
+ 'success',
+ 'Solicitud de vacaciones enviada correctamente.'
+ );
+ }
+ } catch (error) {
+ if (getApiUrl().environment === 'development') {
+ console.error(error);
+ }
+ } finally {
+ hideProgressbar();
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default VacationsRequest;
diff --git a/frontend/src/components/shared/BirthdaySlider.jsx b/frontend/src/components/shared/BirthdaySlider.jsx
new file mode 100644
index 00000000..d1e25d36
--- /dev/null
+++ b/frontend/src/components/shared/BirthdaySlider.jsx
@@ -0,0 +1,34 @@
+// import Swiper core and required modules
+import { Navigation, Pagination, Scrollbar, A11y } from 'swiper/modules';
+
+import { Swiper, SwiperSlide } from 'swiper/react';
+
+// Import Swiper styles
+import 'swiper/css';
+import 'swiper/css/navigation';
+import 'swiper/css/pagination';
+import 'swiper/css/scrollbar';
+
+export const BirthdaySlider = () => {
+ return (
+ console.log(swiper)}
+ onSlideChange={() => console.log('slide change')}
+ className="mySwiper"
+ >
+ Slide 1
+ Slide 2
+ Slide 3
+ Slide 4
+
+ );
+};
+
+export default BirthdaySlider;
diff --git a/frontend/src/components/shared/embla-carousel/EmblaCarousel.jsx b/frontend/src/components/shared/embla-carousel/EmblaCarousel.jsx
index 1250d5ae..feac3338 100644
--- a/frontend/src/components/shared/embla-carousel/EmblaCarousel.jsx
+++ b/frontend/src/components/shared/embla-carousel/EmblaCarousel.jsx
@@ -94,8 +94,8 @@ export function EmblaCarousel() {
style={{
display: 'flex',
touchAction: 'pan-y pinch-zoom',
- height: '750px',
width: '1280px',
+ height: '750px',
}}
>
{images.map((image, index) => (
diff --git a/frontend/src/components/shared/embla-carousel/EmblaCarouselLazyLoadImage.jsx b/frontend/src/components/shared/embla-carousel/EmblaCarouselLazyLoadImage.jsx
index 17242e6f..7215d985 100644
--- a/frontend/src/components/shared/embla-carousel/EmblaCarouselLazyLoadImage.jsx
+++ b/frontend/src/components/shared/embla-carousel/EmblaCarouselLazyLoadImage.jsx
@@ -1,118 +1,118 @@
-import React, { useState, useCallback } from 'react';
-
-// Material-UI
-import { Box } from '@mui/material';
-
-// Icons
-import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
-import AddIcon from '@mui/icons-material/Add';
-import IconButton from '@mui/material/IconButton';
-
-// Custom Hooks
-import { useSnackbar } from '@contexts/SnackbarContext';
-
-// Custom Functions and Components
-import { getApiUrl } from '@assets/getApi';
-import { handleError } from '@assets/handleError';
-
-const PLACEHOLDER_SRC = `data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D`;
-
-const IconButtonsStyle = {
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
- color: 'white',
- '&:hover': {
- backgroundColor: 'white',
- color: 'gray',
- },
- transition: 'all 0.3s',
-};
-
-export const LazyLoadImage = (props) => {
- const { image, inView, setOpenAddDialog, setImages, getCarouselImages } =
- props;
- const [hasLoaded, setHasLoaded] = useState(false);
- const permissions = JSON.parse(localStorage.getItem('permissions')) || [];
- const { showSnack } = useSnackbar();
-
- const setLoaded = useCallback(() => {
- if (inView) setHasLoaded(true);
- }, [inView, setHasLoaded]);
-
- const deleteCarouselImage = async (id) => {
- try {
- const response = await fetch(
- `${getApiUrl().apiUrl}carousel-images/banners/${id}/`,
- {
- method: 'DELETE',
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- },
- }
- );
-
- await handleError(response, showSnack);
-
- if (response.status === 204) {
- showSnack('success', 'Imagen eliminada correctamente');
- getCarouselImages(setImages, showSnack);
- }
- } catch (error) {
- if (getApiUrl().environment === 'development') {
- console.error(error);
- }
- }
- };
-
- return (
-