diff --git a/Dockerfile b/Dockerfile index 5b491a37f..632144db7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:latest +FROM python:3.10-slim ENV PYTHONUNBUFFERED 1 ENV DJANGO_SETTINGS_MODULE="chapp.settings" diff --git a/db.sqlite3 b/db.sqlite3 index 28c05bb8e..2a85ca917 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/pms/form_dates/__pycache__/Ymd.cpython-310.pyc b/pms/form_dates/__pycache__/Ymd.cpython-310.pyc index 92187ad49..d5b5a51fa 100644 Binary files a/pms/form_dates/__pycache__/Ymd.cpython-310.pyc and b/pms/form_dates/__pycache__/Ymd.cpython-310.pyc differ diff --git a/pms/forms.py b/pms/forms.py index f1bc68d08..ed3670e0a 100644 --- a/pms/forms.py +++ b/pms/forms.py @@ -1,9 +1,11 @@ -from datetime import datetime +from datetime import date, datetime from django import forms from django.forms import ModelForm from .models import Booking, Customer +MAX_BOOKING_DATE = date(2026, 12, 31) + class RoomSearchForm(ModelForm): class Meta: @@ -56,3 +58,59 @@ class Meta: 'total': forms.HiddenInput(), 'state': forms.HiddenInput(), } + + +class BookingDatesForm(ModelForm): + class Meta: + model = Booking + fields = ["checkin", "checkout"] + labels = { + "checkin": "Fecha entrada", + "checkout": "Fecha salida", + } + widgets = { + "checkin": forms.DateInput( + attrs={ + "type": "date", + "min": date.today().strftime("%Y-%m-%d"), + "max": MAX_BOOKING_DATE.strftime("%Y-%m-%d"), + } + ), + "checkout": forms.DateInput( + attrs={ + "type": "date", + "min": date.today().strftime("%Y-%m-%d"), + "max": MAX_BOOKING_DATE.strftime("%Y-%m-%d"), + } + ), + } + + def clean(self): + cleaned_data = super().clean() + checkin = cleaned_data.get("checkin") + checkout = cleaned_data.get("checkout") + + today = date.today() + + if checkin and checkin < today: + self.add_error("checkin", "La fecha de entrada no puede ser anterior a hoy.") + if checkout and checkout < today: + self.add_error("checkout", "La fecha de salida no puede ser anterior a hoy.") + + if checkin and checkin > MAX_BOOKING_DATE: + self.add_error( + "checkin", + f"La fecha de entrada no puede ser posterior a {MAX_BOOKING_DATE.strftime('%d/%m/%Y')}.", + ) + if checkout and checkout > MAX_BOOKING_DATE: + self.add_error( + "checkout", + f"La fecha de salida no puede ser posterior a {MAX_BOOKING_DATE.strftime('%d/%m/%Y')}.", + ) + + if checkin and checkout and checkin >= checkout: + raise forms.ValidationError( + "La fecha de salida debe ser posterior a la fecha de entrada." + ) + + return cleaned_data diff --git a/pms/migrations/__pycache__/0001_initial.cpython-310.pyc b/pms/migrations/__pycache__/0001_initial.cpython-310.pyc index a99f1f151..ea38fe488 100644 Binary files a/pms/migrations/__pycache__/0001_initial.cpython-310.pyc and b/pms/migrations/__pycache__/0001_initial.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0002_book_created_room_description_room_name.cpython-310.pyc b/pms/migrations/__pycache__/0002_book_created_room_description_room_name.cpython-310.pyc index 58da46f64..979de1518 100644 Binary files a/pms/migrations/__pycache__/0002_book_created_room_description_room_name.cpython-310.pyc and b/pms/migrations/__pycache__/0002_book_created_room_description_room_name.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0003_room_type_remove_room_max_guests_remove_room_price_and_more.cpython-310.pyc b/pms/migrations/__pycache__/0003_room_type_remove_room_max_guests_remove_room_price_and_more.cpython-310.pyc index d66eeeb15..5cb57f65f 100644 Binary files a/pms/migrations/__pycache__/0003_room_type_remove_room_max_guests_remove_room_price_and_more.cpython-310.pyc and b/pms/migrations/__pycache__/0003_room_type_remove_room_max_guests_remove_room_price_and_more.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0004_book_state_alter_book_code.cpython-310.pyc b/pms/migrations/__pycache__/0004_book_state_alter_book_code.cpython-310.pyc index 985835c42..e86078e9d 100644 Binary files a/pms/migrations/__pycache__/0004_book_state_alter_book_code.cpython-310.pyc and b/pms/migrations/__pycache__/0004_book_state_alter_book_code.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0005_alter_book_code_alter_book_state.cpython-310.pyc b/pms/migrations/__pycache__/0005_alter_book_code_alter_book_state.cpython-310.pyc index b104a2c2d..38b7ec1cc 100644 Binary files a/pms/migrations/__pycache__/0005_alter_book_code_alter_book_state.cpython-310.pyc and b/pms/migrations/__pycache__/0005_alter_book_code_alter_book_state.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0006_alter_book_code_alter_book_state.cpython-310.pyc b/pms/migrations/__pycache__/0006_alter_book_code_alter_book_state.cpython-310.pyc index c52078fb2..9bb899cbe 100644 Binary files a/pms/migrations/__pycache__/0006_alter_book_code_alter_book_state.cpython-310.pyc and b/pms/migrations/__pycache__/0006_alter_book_code_alter_book_state.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0007_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc b/pms/migrations/__pycache__/0007_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc index b70e3b9ac..2f77d51e1 100644 Binary files a/pms/migrations/__pycache__/0007_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc and b/pms/migrations/__pycache__/0007_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0008_alter_book_code.cpython-310.pyc b/pms/migrations/__pycache__/0008_alter_book_code.cpython-310.pyc index 4671a97f8..4792c2044 100644 Binary files a/pms/migrations/__pycache__/0008_alter_book_code.cpython-310.pyc and b/pms/migrations/__pycache__/0008_alter_book_code.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0009_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc b/pms/migrations/__pycache__/0009_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc index b2a360a86..69805694b 100644 Binary files a/pms/migrations/__pycache__/0009_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc and b/pms/migrations/__pycache__/0009_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0010_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc b/pms/migrations/__pycache__/0010_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc index e13fb03a9..699e06334 100644 Binary files a/pms/migrations/__pycache__/0010_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc and b/pms/migrations/__pycache__/0010_alter_book_checkin_alter_book_checkout_and_more.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0011_alter_book_code.cpython-310.pyc b/pms/migrations/__pycache__/0011_alter_book_code.cpython-310.pyc index b7d323daf..68a48c1b5 100644 Binary files a/pms/migrations/__pycache__/0011_alter_book_code.cpython-310.pyc and b/pms/migrations/__pycache__/0011_alter_book_code.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0012_booking_delete_book.cpython-310.pyc b/pms/migrations/__pycache__/0012_booking_delete_book.cpython-310.pyc index adbd8d531..f5db74384 100644 Binary files a/pms/migrations/__pycache__/0012_booking_delete_book.cpython-310.pyc and b/pms/migrations/__pycache__/0012_booking_delete_book.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0013_alter_booking_code.cpython-310.pyc b/pms/migrations/__pycache__/0013_alter_booking_code.cpython-310.pyc index d88b07d3b..b6b25222b 100644 Binary files a/pms/migrations/__pycache__/0013_alter_booking_code.cpython-310.pyc and b/pms/migrations/__pycache__/0013_alter_booking_code.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/0014_alter_booking_code.cpython-310.pyc b/pms/migrations/__pycache__/0014_alter_booking_code.cpython-310.pyc index 0a0804861..66dfc01a9 100644 Binary files a/pms/migrations/__pycache__/0014_alter_booking_code.cpython-310.pyc and b/pms/migrations/__pycache__/0014_alter_booking_code.cpython-310.pyc differ diff --git a/pms/migrations/__pycache__/__init__.cpython-310.pyc b/pms/migrations/__pycache__/__init__.cpython-310.pyc index 4e4f7df87..d5a092966 100644 Binary files a/pms/migrations/__pycache__/__init__.cpython-310.pyc and b/pms/migrations/__pycache__/__init__.cpython-310.pyc differ diff --git a/pms/reservation_code/__pycache__/generate.cpython-310.pyc b/pms/reservation_code/__pycache__/generate.cpython-310.pyc index 537e81525..f712bda6d 100644 Binary files a/pms/reservation_code/__pycache__/generate.cpython-310.pyc and b/pms/reservation_code/__pycache__/generate.cpython-310.pyc differ diff --git a/pms/templates/dashboard.html b/pms/templates/dashboard.html index 10f0285cc..a9ca00581 100644 --- a/pms/templates/dashboard.html +++ b/pms/templates/dashboard.html @@ -22,6 +22,11 @@

{{dashboard.outcoming_guests}}

Total facturado

€ {% if dashboard.invoiced.total__sum == None %}0.00{% endif %} {{dashboard.invoiced.total__sum|floatformat:2}}

+ +
+
% ocupación
+

{{dashboard.occupancy_pct|floatformat:2}}%

+
{% endblock content%} \ No newline at end of file diff --git a/pms/templates/edit_booking_dates.html b/pms/templates/edit_booking_dates.html new file mode 100644 index 000000000..f63988e5d --- /dev/null +++ b/pms/templates/edit_booking_dates.html @@ -0,0 +1,43 @@ +{% extends "main.html"%} + +{% block content %} +

Editar fechas

+ +
+
Reserva: {{booking.code}}
+
Habitación: {{booking.room}}
+
+ +
+ {% csrf_token%} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} +
{{error}}
+ {% endfor %} +
+ {% endif %} + + {% for field in form %} +
+
+ {{field.label_tag}} +
+
+ {{field}} +
+
+ {% for error in field.errors %} +
{{error}}
+ {% endfor %} +
+
+ {% endfor %} + + Volver + +
+ +{% endblock content%} + diff --git a/pms/templates/home.html b/pms/templates/home.html index 1e61b8024..1be2c0518 100644 --- a/pms/templates/home.html +++ b/pms/templates/home.html @@ -68,7 +68,9 @@

Reservas Realizadas

Editar datos de contacto
- + {% if booking.state != "DEL" %} + Editar fechas + {% endif %}
diff --git a/pms/templates/rooms.html b/pms/templates/rooms.html index c30929f1f..503e80749 100644 --- a/pms/templates/rooms.html +++ b/pms/templates/rooms.html @@ -2,6 +2,25 @@ {% block content %}

Habitaciones del hotel

+ +
+
+ + +
+
+ + Limpiar +
+
+ {% for room in rooms%}
diff --git a/pms/tests.py b/pms/tests.py index 7ce503c2d..c41f74e44 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,3 +1,304 @@ -from django.test import TestCase +from datetime import date, timedelta -# Create your tests here. +from django.test import TestCase, override_settings +from django.urls import reverse + +from .forms import MAX_BOOKING_DATE +from .models import Booking, Customer, Room, Room_type + + +@override_settings( + STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage" +) +class RoomsFilterTests(TestCase): + def setUp(self): + self.room_type = Room_type.objects.create( + name="TestType", + price=10.0, + max_guests=2, + ) + Room.objects.create( + room_type=self.room_type, + name="Room 1.1", + description="Test room 1.1", + ) + Room.objects.create( + room_type=self.room_type, + name="Room 1.2", + description="Test room 1.2", + ) + Room.objects.create( + room_type=self.room_type, + name="Room 2.1", + description="Test room 2.1", + ) + + def test_rooms_without_filter_lists_all_rooms(self): + url = reverse("rooms") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Room 1.1") + self.assertContains(resp, "Room 1.2") + self.assertContains(resp, "Room 2.1") + + def test_rooms_filter_partial_name(self): + url = reverse("rooms") + resp = self.client.get(url, {"name": "Room 1"}) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Room 1.1") + self.assertContains(resp, "Room 1.2") + self.assertNotContains(resp, "Room 2.1") + + def test_rooms_filter_is_case_insensitive(self): + url = reverse("rooms") + resp = self.client.get(url, {"name": "room 1"}) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Room 1.1") + self.assertContains(resp, "Room 1.2") + self.assertNotContains(resp, "Room 2.1") + + def test_rooms_filter_empty_or_whitespace_behaves_like_no_filter(self): + url = reverse("rooms") + resp = self.client.get(url, {"name": " "}) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Room 1.1") + self.assertContains(resp, "Room 1.2") + self.assertContains(resp, "Room 2.1") + + +@override_settings( + STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage" +) +class DashboardOccupancyTests(TestCase): + def setUp(self): + self.room_type = Room_type.objects.create( + name="TestType", + price=10.0, + max_guests=2, + ) + self.rooms = [ + Room.objects.create( + room_type=self.room_type, + name=f"Room {i}", + description=f"Test room {i}", + ) + for i in range(1, 5) + ] + self.customer = Customer.objects.create( + name="Test Customer", + email="test@example.com", + phone="123", + ) + + def test_dashboard_occupancy_pct_counts_only_new_bookings_occupying_today(self): + today = date.today() + + # Counts: state NEW and checkin <= today < checkout + Booking.objects.create( + state="NEW", + checkin=today - timedelta(days=1), + checkout=today + timedelta(days=1), + room=self.rooms[0], + guests=1, + customer=self.customer, + total=10.0, + code="ABCDEFG1", + ) + + # Does not count: outside of today range + Booking.objects.create( + state="NEW", + checkin=today + timedelta(days=5), + checkout=today + timedelta(days=6), + room=self.rooms[1], + guests=1, + customer=self.customer, + total=10.0, + code="ABCDEFG2", + ) + + # Does not count: cancelled + Booking.objects.create( + state="DEL", + checkin=today - timedelta(days=1), + checkout=today + timedelta(days=1), + room=self.rooms[2], + guests=1, + customer=self.customer, + total=10.0, + code="ABCDEFG3", + ) + + url = reverse("dashboard") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + + occupancy_pct = resp.context["dashboard"]["occupancy_pct"] + self.assertAlmostEqual(occupancy_pct, 25.0, places=2) + + def test_dashboard_occupancy_pct_is_zero_when_no_rooms_exist(self): + Room.objects.all().delete() + + url = reverse("dashboard") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + + occupancy_pct = resp.context["dashboard"]["occupancy_pct"] + self.assertEqual(occupancy_pct, 0) + + +@override_settings( + STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage" +) +class EditBookingDatesTests(TestCase): + def setUp(self): + self.room_type = Room_type.objects.create( + name="TestType", + price=20.0, + max_guests=2, + ) + self.room = Room.objects.create( + room_type=self.room_type, + name="Room 1.1", + description="Test room", + ) + self.customer = Customer.objects.create( + name="Test Customer", + email="test@example.com", + phone="123", + ) + + def test_get_edit_dates_page_renders(self): + today = date.today() + booking = Booking.objects.create( + state="NEW", + checkin=today + timedelta(days=1), + checkout=today + timedelta(days=3), + room=self.room, + guests=1, + customer=self.customer, + total=40.0, + code="BOOKTEST", + ) + + url = reverse("edit_booking_dates", kwargs={"pk": booking.id}) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Editar fechas") + + def test_edit_dates_success_updates_dates_and_total(self): + today = date.today() + booking = Booking.objects.create( + state="NEW", + checkin=today + timedelta(days=1), + checkout=today + timedelta(days=3), + room=self.room, + guests=1, + customer=self.customer, + total=40.0, + code="BOOKTES1", + ) + + new_checkin = today + timedelta(days=2) + new_checkout = today + timedelta(days=5) + url = reverse("edit_booking_dates", kwargs={"pk": booking.id}) + resp = self.client.post( + url, + data={ + "checkin": new_checkin.strftime("%Y-%m-%d"), + "checkout": new_checkout.strftime("%Y-%m-%d"), + }, + ) + self.assertEqual(resp.status_code, 302) + + booking.refresh_from_db() + self.assertEqual(booking.checkin, new_checkin) + self.assertEqual(booking.checkout, new_checkout) + self.assertAlmostEqual(booking.total, 60.0, places=2) # 3 nights * 20.0 + + def test_edit_dates_rejects_when_room_not_available(self): + today = date.today() + booking = Booking.objects.create( + state="NEW", + checkin=today + timedelta(days=1), + checkout=today + timedelta(days=3), + room=self.room, + guests=1, + customer=self.customer, + total=40.0, + code="BOOKTES2", + ) + # Another confirmed booking occupying the desired new range + Booking.objects.create( + state="NEW", + checkin=today + timedelta(days=4), + checkout=today + timedelta(days=6), + room=self.room, + guests=1, + customer=self.customer, + total=40.0, + code="BOOKCON1", + ) + + url = reverse("edit_booking_dates", kwargs={"pk": booking.id}) + resp = self.client.post( + url, + data={ + "checkin": (today + timedelta(days=5)).strftime("%Y-%m-%d"), + "checkout": (today + timedelta(days=7)).strftime("%Y-%m-%d"), + }, + ) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "No hay disponibilidad para las fechas seleccionadas") + + booking.refresh_from_db() + self.assertEqual(booking.checkin, today + timedelta(days=1)) + self.assertEqual(booking.checkout, today + timedelta(days=3)) + + def test_edit_dates_rejects_invalid_range(self): + today = date.today() + booking = Booking.objects.create( + state="NEW", + checkin=today + timedelta(days=1), + checkout=today + timedelta(days=3), + room=self.room, + guests=1, + customer=self.customer, + total=40.0, + code="BOOKTES3", + ) + + url = reverse("edit_booking_dates", kwargs={"pk": booking.id}) + resp = self.client.post( + url, + data={ + "checkin": (today + timedelta(days=5)).strftime("%Y-%m-%d"), + "checkout": (today + timedelta(days=5)).strftime("%Y-%m-%d"), + }, + ) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "La fecha de salida debe ser posterior a la fecha de entrada.") + + def test_edit_dates_rejects_after_max_date(self): + today = date.today() + booking = Booking.objects.create( + state="NEW", + checkin=today + timedelta(days=1), + checkout=today + timedelta(days=3), + room=self.room, + guests=1, + customer=self.customer, + total=40.0, + code="BOOKTES4", + ) + + url = reverse("edit_booking_dates", kwargs={"pk": booking.id}) + resp = self.client.post( + url, + data={ + "checkin": MAX_BOOKING_DATE.strftime("%Y-%m-%d"), + "checkout": (MAX_BOOKING_DATE + timedelta(days=1)).strftime("%Y-%m-%d"), + }, + ) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "La fecha de salida no puede ser posterior") diff --git a/pms/urls.py b/pms/urls.py index c18714abf..2acbd0a22 100644 --- a/pms/urls.py +++ b/pms/urls.py @@ -8,6 +8,7 @@ path("search/booking/", views.BookingSearchView.as_view(), name="booking_search"), path("booking//", views.BookingView.as_view(), name="booking"), path("booking//edit", views.EditBookingView.as_view(), name="edit_booking"), + path("booking//edit-dates", views.EditBookingDatesView.as_view(), name="edit_booking_dates"), path("booking//delete", views.DeleteBookingView.as_view(), name="delete_booking"), path("rooms/", views.RoomsView.as_view(), name="rooms"), path("room//", views.RoomDetailsView.as_view(), name="room_details"), diff --git a/pms/views.py b/pms/views.py index f38563933..a9074e5fa 100644 --- a/pms/views.py +++ b/pms/views.py @@ -174,6 +174,53 @@ def post(self, request, pk): return redirect("/") +class EditBookingDatesView(View): + def get(self, request, pk): + booking = Booking.objects.get(id=pk) + if booking.state == "DEL": + return redirect("/") + form = BookingDatesForm(instance=booking) + context = { + "booking": booking, + "form": form, + } + return render(request, "edit_booking_dates.html", context) + + @method_decorator(ensure_csrf_cookie) + def post(self, request, pk): + booking = Booking.objects.get(id=pk) + if booking.state == "DEL": + return redirect("/") + + form = BookingDatesForm(request.POST, instance=booking) + if form.is_valid(): + checkin = form.cleaned_data["checkin"] + checkout = form.cleaned_data["checkout"] + + if not booking.room_id: + form.add_error(None, "La reserva no tiene habitación asignada.") + else: + has_conflict = (Booking.objects + .filter(room_id=booking.room_id, state="NEW") + .exclude(id=booking.id) + .filter(checkin__lt=checkout, checkout__gt=checkin) + .exists()) + if has_conflict: + form.add_error(None, "No hay disponibilidad para las fechas seleccionadas") + else: + updated_booking = form.save(commit=False) + total_days = (checkout - checkin).days + updated_booking.total = total_days * booking.room.room_type.price + updated_booking.save() + return redirect("/") + + context = { + "booking": booking, + "form": form, + } + return render(request, "edit_booking_dates.html", context) + + class DashboardView(View): def get(self, request): from datetime import date, time, datetime @@ -209,12 +256,22 @@ def get(self, request): .aggregate(Sum('total')) ) + total_rooms = Room.objects.count() + occupied_today = (Booking.objects + .filter(state="NEW", checkin__lte=today, checkout__gt=today) + .values("id") + ).count() + occupancy_pct = 0 + if total_rooms: + occupancy_pct = occupied_today / total_rooms * 100 + # preparing context data dashboard = { 'new_bookings': new_bookings, 'incoming_guests': incoming, 'outcoming_guests': outcoming, - 'invoiced': invoiced + 'invoiced': invoiced, + 'occupancy_pct': occupancy_pct, } @@ -239,8 +296,13 @@ def get(self, request, pk): class RoomsView(View): def get(self, request): # renders a list of rooms - rooms = Room.objects.all().values("name", "room_type__name", "id") + name = (request.GET.get("name") or "").strip() + rooms_qs = Room.objects.all() + if name: + rooms_qs = rooms_qs.filter(name__icontains=name) + rooms = rooms_qs.order_by("name").values("name", "room_type__name", "id") context = { - 'rooms': rooms + 'rooms': rooms, + 'name': name, } return render(request, "rooms.html", context) diff --git a/requermientos.md b/requermientos.md new file mode 100644 index 000000000..9e28d0081 --- /dev/null +++ b/requermientos.md @@ -0,0 +1,85 @@ +**Prueba de selección** + +*Buscador de reservas* + +El proyecto consiste en una aplicación Django de un motor de reservas para un hotel que ya ha sido desarrollada y alojada en Github. + +**Enlace al repositorio**: https://github.com/oscarchapp/testbookingengine + +En el *README* del repositorio se encuentra la información para desplegar la aplicación en local. También se incluye un backup de SQLite con la configuración inicial. + +**Descripción del proyecto actual** + +La pantalla principal de la aplicación es un listado de reservas. + +Una reserva consiste de: + +● fecha entrada + +● fecha salida + +● tipo de habitación + +● nº huéspedes + +● datos de contacto (nombre, email, teléfono) + +● precio total de la reserva + +● localizador + +● nº de habitación (opcional) + +La funcionalidad principal es la de crear una nueva reserva. En la parte superior hay un botón de “Nueva reserva” que lleva a una pantalla que permite introducir 2 fechas (entrada y salida) y el número de huéspedes. Al buscar entre 2 fechas se muestra la siguiente información: ● Tipo de habitación + +● Nº de habitaciones disponibles para este rango de fechas + +● Precio total de la estancia + +● Un botón para seleccionar esa habitación + +Tras seleccionar la habitación deseada, se muestra un formulario donde se debe introducir los datos de contacto para la reserva (nombre, email, teléfono). Al finalizar el formulario se creará la reserva (con un localizador alfanumérico único) y se mostrará la pantalla con el listado de reservas. +Algunos detalles a tener en cuenta: + +● Hay 4 tipos de habitaciones: 10 individuales, 5 dobles, 4 triples, 6 cuádruples. ● El precio diario de cada tipo de habitación es: individual=20€/día, doble=30€/día, triple=40€/día, cuádruple=50€/día + +● En una habitación individual sólo cabe 1 persona (huésped). En una doble caben 1 o 2 personas. En una Triple caben 1, 2, o 3 personas. En una cuádruple caben 1, 2, 3 o 4 personas. Por lo tanto si el usuario hace una búsqueda para 3 personas, solo deberá mostrar habitaciones triples y cuádruples (siempre y cuando estén disponibles para esas fechas) . + +● A medida que se vayan creando reservas, debe descontarse del número de habitaciones disponibles de ese tipo para ese rango de fechas. Solo se podrán crear reservas desde la fecha actual hasta el 31/12/2026. + +**Requisitos de la prueba:** + +A continuación, se describen algunos ejercicios sobre cambios de funcionalidad en la aplicación existente. Se requiere que el candidato cree pull-request por cada uno y se valorará la calidad del código, buenas prácticas y la inclusión de tests. + +Cualquier cambio añadido sobre lo aquí explicado que suponga una mejora en la aplicación será bienvenida y tenida en cuenta. + +1\. **Filtrar panel de habitaciones**: + +En la sección de Habitaciones, actualmente se muestra una lista completa de todas las habitaciones existentes, con su nombre (ej Room 1.1). + +Se debe añadir un formulario en la parte superior para poder filtrar los resultados de la lista, permitiendo buscar únicamente por el nombre de la habitación. Dicho filtro debe buscar habitaciones cuyo campo “name” contenga el texto introducido en el formulario. Por ejemplo, si se introduce “Room 1”, se mostrarán las habitaciones “Room 1.1”, “Room 1.2”, etc, pero no otras como “Room 2.1”. + +*Nota*: se puede realizar de cualquier forma, bien por un formulario HTML que envíe un POST a la view, con Javascript y AJAX o cualquier otra que el candidato considere mejor. El diseño de la interfaz no es importante, puede ser un formulario básico sin estilo. + +**2\. Añadir porcentaje de ocupación** + +En la sección de Dashboard, se muestran varios “widgets” con datos de las reservas actuales. +Se requiere añadir un nuevo widget llamado “% ocupación”, cuyo cálculo es el siguiente: *Cantidad total de reservas en estado confirmada / número de habitaciones existentes.* + +Nota: Adaptar el diseño del Dashboard para mostrar este nuevo valor en la interfaz, alineado al resto de elementos o de la forma que considere mejor el candidato. + +**3\. Edición de reservas** + +En la pantalla principal se muestran todas las reservas realizadas y se permite actualmente poder editar los datos de contacto. + +Se requiere añadir una nueva funcionalidad para editar las fechas de la reserva, añadiendo un enlace a cada reserva similar al de “Editar datos de contacto”. Dicho enlace debe llevar una nueva página con un formulario similar al de crear nueva reserva, pero solo con los campos de fecha de entrada y salida y un botón de “Guardar”. + +Además de editar los campos de la reserva, se debe realizar la misma validación que al crear una nueva reserva, es decir, comprobar que la habitación actual está disponible en las nuevas fechas seleccionadas. Si existen otras reservas ocupando en las mismas fechas, se debe mostrar un error: “No hay disponibilidad para las fechas seleccionadas”. + +**Instrucciones para la prueba técnica** + +En el repositorio mencionado encontrarás el punto de partida para la prueba donde debes crear los pull requests. + +Tu objetivo es completar las 3 tareas que indicamos en este documento. No hay una única forma correcta de abordarla. Nos interesa ver cómo trabajas, cómo estructuras tus cambios y cómo los presentas al equipo. + +Piensa en esta prueba como una pequeña simulación del día a día en el trabajo. Cuando hayas terminado, recuerda indicarnos tu nombre de usuario en Github. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 148e2c607..1537a31e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ asgiref==3.5.0 Django==4.0.2 +gunicorn==20.1.0 sqlparse==0.4.2 -whitenoise +whitenoise==6.0.0