From cb95c780d0c44bec9565e0bd9a907c154872f361 Mon Sep 17 00:00:00 2001 From: Andriy Yatsyk Gavrylyak Date: Fri, 27 Mar 2026 19:12:43 +0100 Subject: [PATCH 1/3] ADD: live_search component in section rooms and tests --- pms/statics/js/live_search.js | 17 +++++++++++++ pms/templates/components/live_search.html | 14 ++++++++++ pms/templates/main.html | 1 + pms/templates/rooms.html | 11 +++++--- pms/tests/__init__.py | 0 pms/tests/test_rooms_view.py | 31 +++++++++++++++++++++++ 6 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 pms/statics/js/live_search.js create mode 100644 pms/templates/components/live_search.html create mode 100644 pms/tests/__init__.py create mode 100644 pms/tests/test_rooms_view.py diff --git a/pms/statics/js/live_search.js b/pms/statics/js/live_search.js new file mode 100644 index 000000000..14fb6a052 --- /dev/null +++ b/pms/statics/js/live_search.js @@ -0,0 +1,17 @@ +// pms/statics/js/live_search.js +document.addEventListener('DOMContentLoaded', function () { + const input = document.getElementById('live-search-input'); + if (!input) return; // el componente no está en esta página, salir + + const targetId = input.dataset.target; + const container = document.getElementById(targetId); + if (!container) return; + + input.addEventListener('input', function () { + const query = input.value.toLowerCase().trim(); + Array.from(container.children).forEach(function (card) { + const text = (card.dataset.search || '').toLowerCase(); + card.style.display = text.includes(query) ? '' : 'none'; + }); + }); +}); diff --git a/pms/templates/components/live_search.html b/pms/templates/components/live_search.html new file mode 100644 index 000000000..f323e7067 --- /dev/null +++ b/pms/templates/components/live_search.html @@ -0,0 +1,14 @@ +{# components/live_search.html #} +{# Uso: {% include "components/live_search.html" with target_id="rooms-list" %} #} +
+
+ +
+
diff --git a/pms/templates/main.html b/pms/templates/main.html index b2216a759..a313cc8b9 100644 --- a/pms/templates/main.html +++ b/pms/templates/main.html @@ -48,5 +48,6 @@ {% endblock %} + \ No newline at end of file diff --git a/pms/templates/rooms.html b/pms/templates/rooms.html index c30929f1f..4f93e3203 100644 --- a/pms/templates/rooms.html +++ b/pms/templates/rooms.html @@ -2,8 +2,12 @@ {% block content %}

Habitaciones del hotel

-{% for room in rooms%} -
+ +{% include "components/live_search.html" with target_id="rooms-list" %} + +
+{% for room in rooms %} +
{{room.name}} ({{room.room_type__name}}) @@ -11,9 +15,8 @@

Habitaciones del hotel

-
-
{% endfor %} +
{% endblock content%} diff --git a/pms/tests/__init__.py b/pms/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pms/tests/test_rooms_view.py b/pms/tests/test_rooms_view.py new file mode 100644 index 000000000..2c445c952 --- /dev/null +++ b/pms/tests/test_rooms_view.py @@ -0,0 +1,31 @@ +from django.test import TestCase, override_settings +from django.urls import reverse +from ..models import Room, Room_type + + +@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage') +class RoomsViewTest(TestCase): + + def setUp(self): + room_type = Room_type.objects.create(name="Suite", price=100, max_guests=2) + Room.objects.create(name="Room 1.1", description="", room_type=room_type) + Room.objects.create(name="Room 1.2", description="", room_type=room_type) + Room.objects.create(name="Room 2.1", description="", room_type=room_type) + + def test_rooms_page_loads(self): + response = self.client.get(reverse("rooms")) + self.assertEqual(response.status_code, 200) + + def test_all_rooms_appear_in_context(self): + response = self.client.get(reverse("rooms")) + self.assertEqual(len(response.context["rooms"]), 3) + + def test_room_names_in_response(self): + response = self.client.get(reverse("rooms")) + self.assertContains(response, "Room 1.1") + self.assertContains(response, "Room 2.1") + + def test_data_search_attribute_rendered(self): + """El atributo data-search debe estar presente para que el JS funcione""" + response = self.client.get(reverse("rooms")) + self.assertContains(response, 'data-search="Room 1.1') From 8c83c2cec9cdaa31472d87e8b7acfe0f24e35027 Mon Sep 17 00:00:00 2001 From: Andriy Yatsyk Gavrylyak Date: Fri, 27 Mar 2026 19:14:08 +0100 Subject: [PATCH 2/3] ADD: occupancy_rate in section dashboard and tests --- pms/services/dashboard_service.py | 53 +++++++++++++++++++++ pms/templates/dashboard.html | 6 ++- pms/tests/test_dashboard_service.py | 71 +++++++++++++++++++++++++++++ pms/views.py | 49 ++------------------ 4 files changed, 133 insertions(+), 46 deletions(-) create mode 100644 pms/services/dashboard_service.py create mode 100644 pms/tests/test_dashboard_service.py diff --git a/pms/services/dashboard_service.py b/pms/services/dashboard_service.py new file mode 100644 index 000000000..a16e8618c --- /dev/null +++ b/pms/services/dashboard_service.py @@ -0,0 +1,53 @@ +from datetime import date, datetime, time +from django.db.models import Sum +from ..models import Booking, Room + + +def get_dashboard_data(): + today = date.today() + + today_min = datetime.combine(today, time.min) + today_max = datetime.combine(today, time.max) + today_range = (today_min, today_max) + + # bookings created today + new_bookings = Booking.objects.filter( + created__range=today_range + ).count() + + # checkin guests + incoming = Booking.objects.filter( + checkin=today + ).exclude(state="DEL").count() + + # checkout guests + outcoming = Booking.objects.filter( + checkout=today + ).exclude(state="DEL").count() + + # invoiced today + invoiced = Booking.objects.filter( + created__range=today_range + ).exclude(state="DEL").aggregate(total=Sum('total'))["total"] or 0 + + # total rooms + total_rooms = Room.objects.count() + + confirmed_bookings = Booking.objects.filter( + state=Booking.NEW, # excluye canceladas aunque las fechas coincidan + checkin__lte=today, + checkout__gt=today, + ).count() + + occupancy_rate = ( + (confirmed_bookings / total_rooms) * 100 + if total_rooms > 0 else 0 + ) + + return { + 'new_bookings': new_bookings, + 'incoming_guests': incoming, + 'outcoming_guests': outcoming, + 'invoiced': invoiced, + 'occupancy_rate': occupancy_rate + } \ No newline at end of file diff --git a/pms/templates/dashboard.html b/pms/templates/dashboard.html index 10f0285cc..4b4dfa159 100644 --- a/pms/templates/dashboard.html +++ b/pms/templates/dashboard.html @@ -20,7 +20,11 @@

{{dashboard.outcoming_guests}}

Total facturado
-

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

+

€ {{ dashboard.invoiced|default:0|floatformat:2 }}

+
+
+
% Ocupación
+

{{dashboard.occupancy_rate|floatformat:1}}%

diff --git a/pms/tests/test_dashboard_service.py b/pms/tests/test_dashboard_service.py new file mode 100644 index 000000000..8fb55d41a --- /dev/null +++ b/pms/tests/test_dashboard_service.py @@ -0,0 +1,71 @@ +from django.test import TestCase +from datetime import date, timedelta +from ..models import Room, Room_type, Booking, Customer +from ..services.dashboard_service import get_dashboard_data + + +class OccupancyRateTest(TestCase): + + def setUp(self): + room_type = Room_type.objects.create(name="Suite", price=100, max_guests=2) + self.customer = Customer.objects.create(name="Test", email="t@t.com", phone="123") + self.room1 = Room.objects.create(name="R1", description="", room_type=room_type) + self.room2 = Room.objects.create(name="R2", description="", room_type=room_type) + self.today = date.today() + + def _make_booking(self, room, state, code, checkin=None, checkout=None): + return Booking.objects.create( + room=room, + customer=self.customer, + state=state, + checkin=checkin or self.today, + checkout=checkout or self.today + timedelta(days=1), + guests=1, + total=100, + code=code, + ) + + def test_occupancy_rate_with_active_booking(self): + """2 habitaciones, 1 activa hoy → 50%""" + self._make_booking(self.room1, Booking.NEW, "BOOK0001") + data = get_dashboard_data() + self.assertAlmostEqual(data['occupancy_rate'], 50.0) + + def test_cancelled_bookings_excluded_from_rate(self): + """Las canceladas no cuentan""" + self._make_booking(self.room1, Booking.DELETED, "BOOK0002") + data = get_dashboard_data() + self.assertEqual(data['occupancy_rate'], 0.0) + + def test_past_bookings_excluded_from_rate(self): + """Reservas con checkout pasado no cuentan""" + self._make_booking( + self.room1, Booking.NEW, "BOOK0003", + checkin=self.today - timedelta(days=3), + checkout=self.today - timedelta(days=1), + ) + data = get_dashboard_data() + self.assertEqual(data['occupancy_rate'], 0.0) + + def test_future_bookings_excluded_from_rate(self): + """Reservas que aún no han empezado no cuentan""" + self._make_booking( + self.room1, Booking.NEW, "BOOK0004", + checkin=self.today + timedelta(days=1), + checkout=self.today + timedelta(days=3), + ) + data = get_dashboard_data() + self.assertEqual(data['occupancy_rate'], 0.0) + + def test_occupancy_rate_100_percent(self): + """Todas las habitaciones activas hoy → 100%""" + self._make_booking(self.room1, Booking.NEW, "BOOK0005") + self._make_booking(self.room2, Booking.NEW, "BOOK0006") + data = get_dashboard_data() + self.assertAlmostEqual(data['occupancy_rate'], 100.0) + + def test_occupancy_rate_no_rooms(self): + """Sin habitaciones no hay división por cero""" + Room.objects.all().delete() + data = get_dashboard_data() + self.assertEqual(data['occupancy_rate'], 0) diff --git a/pms/views.py b/pms/views.py index 14971c8cc..cd84107fd 100644 --- a/pms/views.py +++ b/pms/views.py @@ -7,6 +7,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie from django.utils.decorators import method_decorator from .reservation_code import generate +from .services.dashboard_service import get_dashboard_data # Create your views here. class BookingSearchView(View): @@ -173,53 +174,11 @@ def post(self,request,pk): return redirect("/") class DashboardView(View): - def get(self,request): - from datetime import date, time, datetime - today=date.today() - - #get bookings created today - today_min = datetime.combine(today, time.min) - today_max = datetime.combine(today, time.max) - today_range=(today_min, today_max) - new_bookings = (Booking.objects - .filter(created__range=today_range) - .values("id") - ).count() - - #get incoming guests - incoming = (Booking.objects - .filter(checkin=today) - .exclude(state="DEL") - .values("id") - ).count() - - #get outcoming guests - outcoming = (Booking.objects - .filter(checkout=today) - .exclude(state="DEL") - .values("id") - ).count() - - #get outcoming guests - invoiced = (Booking.objects - .filter(created__range=today_range) - .exclude(state="DEL") - .aggregate(Sum('total')) - ) - - #preparing context data - dashboard = { - 'new_bookings':new_bookings, - 'incoming_guests':incoming, - 'outcoming_guests':outcoming, - 'invoiced':invoiced - - } - print(dashboard) + def get(self, request): context = { - 'dashboard':dashboard + 'dashboard': get_dashboard_data() } - return render(request,"dashboard.html",context) + return render(request, "dashboard.html", context) class RoomDetailsView(View): def get(self,request,pk): From 99c8c91b0a49c9afde6517f33183aee434696f64 Mon Sep 17 00:00:00 2001 From: Andriy Yatsyk Gavrylyak Date: Fri, 27 Mar 2026 21:04:01 +0100 Subject: [PATCH 3/3] ADD: edit booking dates with availability validation --- pms/forms.py | 13 ++ pms/services/booking_service.py | 19 +++ pms/services/dashboard_service.py | 5 +- pms/templates/edit_booking_date.html | 35 +++++ pms/templates/home.html | 7 +- pms/tests/test_booking_date_edit.py | 112 +++++++++++++++ pms/tests/test_booking_service.py | 76 ++++++++++ pms/urls.py | 1 + pms/views.py | 202 --------------------------- pms/views/__init__.py | 11 ++ pms/views/booking.py | 122 ++++++++++++++++ pms/views/dashboard.py | 8 ++ pms/views/room.py | 66 +++++++++ pms/views/search.py | 22 +++ 14 files changed, 491 insertions(+), 208 deletions(-) create mode 100644 pms/services/booking_service.py create mode 100644 pms/templates/edit_booking_date.html create mode 100644 pms/tests/test_booking_date_edit.py create mode 100644 pms/tests/test_booking_service.py delete mode 100644 pms/views.py create mode 100644 pms/views/__init__.py create mode 100644 pms/views/booking.py create mode 100644 pms/views/dashboard.py create mode 100644 pms/views/room.py create mode 100644 pms/views/search.py diff --git a/pms/forms.py b/pms/forms.py index 3607a95b0..8b7e4dae1 100644 --- a/pms/forms.py +++ b/pms/forms.py @@ -51,3 +51,16 @@ class Meta: 'state':forms.HiddenInput(), } +class BookingDateForm(ModelForm): + class Meta: + model = Booking + fields = ['checkin', 'checkout'] + labels = { + 'checkin': 'Entrada', + 'checkout': 'Salida' + } + widgets = { + 'checkin': forms.DateInput(attrs={'type': 'date'}), + 'checkout': forms.DateInput(attrs={'type': 'date'}), + } + diff --git a/pms/services/booking_service.py b/pms/services/booking_service.py new file mode 100644 index 000000000..5d2cfb3e7 --- /dev/null +++ b/pms/services/booking_service.py @@ -0,0 +1,19 @@ +from ..models import Booking + + +def is_room_available(room, checkin, checkout, exclude_booking_id=None): + """ + Valida si una habitación está disponible en las fechas especificadas. + bool: True si disponible, False si hay conflicto + """ + conflicts = Booking.objects.filter( + state=Booking.NEW, + room=room, + checkin__lte=checkout, + checkout__gte=checkin, + ) + + if exclude_booking_id: + conflicts = conflicts.exclude(id=exclude_booking_id) + + return not conflicts.exists() diff --git a/pms/services/dashboard_service.py b/pms/services/dashboard_service.py index a16e8618c..739567f1c 100644 --- a/pms/services/dashboard_service.py +++ b/pms/services/dashboard_service.py @@ -1,13 +1,14 @@ from datetime import date, datetime, time from django.db.models import Sum +from django.utils.timezone import make_aware from ..models import Booking, Room def get_dashboard_data(): today = date.today() - today_min = datetime.combine(today, time.min) - today_max = datetime.combine(today, time.max) + today_min = make_aware(datetime.combine(today, time.min)) + today_max = make_aware(datetime.combine(today, time.max)) today_range = (today_min, today_max) # bookings created today diff --git a/pms/templates/edit_booking_date.html b/pms/templates/edit_booking_date.html new file mode 100644 index 000000000..d5daadaa6 --- /dev/null +++ b/pms/templates/edit_booking_date.html @@ -0,0 +1,35 @@ +{% extends "main.html"%} + +{% block content %} +

Editar fechas

+
+
+ {% csrf_token %} + {% for field in date_form %} +
+
+ {{field.label_tag}} +
+
+ {{field}} +
+
+ {% endfor %} + + {% if date_form.non_field_errors %} + + {% endif %} + +
+
+ Cancelar + +
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/pms/templates/home.html b/pms/templates/home.html index 1e61b8024..116a752d0 100644 --- a/pms/templates/home.html +++ b/pms/templates/home.html @@ -64,19 +64,18 @@

Reservas Realizadas

- + {% if booking.state != "DEL" %} + Editar fechas + {% endif %}
- {% if booking.state != "DEL" %} Cancelar reserva {% endif %}
-
diff --git a/pms/tests/test_booking_date_edit.py b/pms/tests/test_booking_date_edit.py new file mode 100644 index 000000000..b6fe1d385 --- /dev/null +++ b/pms/tests/test_booking_date_edit.py @@ -0,0 +1,112 @@ +from django.test import TestCase, override_settings +from django.urls import reverse +from datetime import date, timedelta +from ..models import Room, Room_type, Booking, Customer + + +@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage') +class EditBookingDateTest(TestCase): + + def setUp(self): + room_type = Room_type.objects.create(name="Suite", price=100, max_guests=2) + self.customer = Customer.objects.create(name="Test", email="t@t.com", phone="123") + self.room = Room.objects.create(name="R1", description="", room_type=room_type) + self.today = date.today() + + self.booking = Booking.objects.create( + room=self.room, + customer=self.customer, + state=Booking.NEW, + checkin=self.today + timedelta(days=5), + checkout=self.today + timedelta(days=7), + guests=2, + total=200, + code="BOOK0001", + ) + + def test_edit_booking_date_success(self): + new_checkin = self.today + timedelta(days=10) + new_checkout = self.today + timedelta(days=12) + + response = self.client.post( + reverse("edit_booking_dates", args=[self.booking.id]), + {'checkin': new_checkin.isoformat(), 'checkout': new_checkout.isoformat()} + ) + + self.booking.refresh_from_db() + self.assertEqual(self.booking.checkin, new_checkin) + self.assertEqual(self.booking.checkout, new_checkout) + self.assertRedirects(response, '/') + + def test_edit_booking_date_conflict_rejected(self): + Booking.objects.create( + room=self.room, + customer=self.customer, + state=Booking.NEW, + checkin=self.today + timedelta(days=15), + checkout=self.today + timedelta(days=18), + guests=2, + total=300, + code="BOOK0002", + ) + + new_checkin = self.today + timedelta(days=14) + new_checkout = self.today + timedelta(days=16) + + response = self.client.post( + reverse("edit_booking_dates", args=[self.booking.id]), + {'checkin': new_checkin.isoformat(), 'checkout': new_checkout.isoformat()} + ) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "No hay disponibilidad") + + def test_cancelled_booking_not_blocking(self): + Booking.objects.create( + room=self.room, + customer=self.customer, + state=Booking.DELETED, + checkin=self.today + timedelta(days=20), + checkout=self.today + timedelta(days=22), + guests=2, + total=200, + code="BOOK0003", + ) + + new_checkin = self.today + timedelta(days=20) + new_checkout = self.today + timedelta(days=22) + + response = self.client.post( + reverse("edit_booking_dates", args=[self.booking.id]), + {'checkin': new_checkin.isoformat(), 'checkout': new_checkout.isoformat()} + ) + + self.assertRedirects(response, '/') + + def test_concurrent_edit_protection(self): + other_booking = Booking.objects.create( + room=self.room, + customer=self.customer, + state=Booking.NEW, + checkin=self.today + timedelta(days=15), + checkout=self.today + timedelta(days=18), + guests=2, + total=300, + code="BOOK0004", + ) + + new_checkin = self.today + timedelta(days=14) + new_checkout = self.today + timedelta(days=16) + + response = self.client.post( + reverse("edit_booking_dates", args=[self.booking.id]), + {'checkin': new_checkin.isoformat(), 'checkout': new_checkout.isoformat()} + ) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "No hay disponibilidad") + + self.booking.refresh_from_db() + self.assertEqual(self.booking.checkin, self.today + timedelta(days=5)) + self.assertEqual(self.booking.checkout, self.today + timedelta(days=7)) + diff --git a/pms/tests/test_booking_service.py b/pms/tests/test_booking_service.py new file mode 100644 index 000000000..1ba21dd24 --- /dev/null +++ b/pms/tests/test_booking_service.py @@ -0,0 +1,76 @@ +from django.test import TestCase +from datetime import date, timedelta +from ..models import Room, Room_type, Booking, Customer +from ..services.booking_service import is_room_available + + +class RoomAvailabilityServiceTest(TestCase): + def setUp(self): + room_type = Room_type.objects.create(name="Suite", price=100, max_guests=2) + self.customer = Customer.objects.create(name="Test", email="t@t.com", phone="123") + self.room = Room.objects.create(name="R1", description="", room_type=room_type) + self.today = date.today() + + def test_room_available_when_no_bookings(self): + result = is_room_available( + self.room, + self.today, + self.today + timedelta(days=2) + ) + self.assertTrue(result) + + def test_room_unavailable_with_conflict(self): + Booking.objects.create( + room=self.room, + customer=self.customer, + state=Booking.NEW, + checkin=self.today, + checkout=self.today + timedelta(days=3), + guests=1, + total=100, + code="EXISTING" + ) + result = is_room_available( + self.room, + self.today + timedelta(days=1), + self.today + timedelta(days=2) + ) + self.assertFalse(result) + + def test_exclude_booking_id_on_edit(self): + booking = Booking.objects.create( + room=self.room, + customer=self.customer, + state=Booking.NEW, + checkin=self.today, + checkout=self.today + timedelta(days=3), + guests=1, + total=100, + code="TEST" + ) + result = is_room_available( + self.room, + self.today, + self.today + timedelta(days=3), + exclude_booking_id=booking.id + ) + self.assertTrue(result) + + def test_cancelled_bookings_ignored(self): + Booking.objects.create( + room=self.room, + customer=self.customer, + state=Booking.DELETED, + checkin=self.today, + checkout=self.today + timedelta(days=3), + guests=1, + total=100, + code="CANCELLED" + ) + result = is_room_available( + self.room, + self.today, + self.today + timedelta(days=2) + ) + self.assertTrue(result) + diff --git a/pms/urls.py b/pms/urls.py index 5f3ebbbca..be3083554 100644 --- a/pms/urls.py +++ b/pms/urls.py @@ -7,6 +7,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.EditBookingDateView.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 deleted file mode 100644 index cd84107fd..000000000 --- a/pms/views.py +++ /dev/null @@ -1,202 +0,0 @@ -from django.shortcuts import render, redirect -from django.views import View -from .models import Room -from .forms import * -from django.db.models import F,Q, Count, Sum -from .form_dates import Ymd -from django.views.decorators.csrf import ensure_csrf_cookie -from django.utils.decorators import method_decorator -from .reservation_code import generate -from .services.dashboard_service import get_dashboard_data -# Create your views here. - -class BookingSearchView(View): - #renders search results for bookingings - def get(self,request): - query = request.GET.dict() - if(not "filter" in query): - return redirect("/") - bookings = (Booking.objects - .filter(Q(code__icontains = query['filter']) | Q(customer__name__icontains = query['filter'])) - .order_by("-created")) - room_search_form = RoomSearchForm() - context = { - 'bookings':bookings, - 'form':room_search_form, - 'filter':True - } - return render(request,"home.html",context) - - -class RoomSearchView(View): - #renders the search form - def get(self,request): - room_search_form = RoomSearchForm() - context = { - 'form':room_search_form - } - - return render(request,"booking_search_form.html",context) - - #renders the search results of available rooms by date and guests - def post(self,request): - query = request.POST.dict() - #calculate number of days in the hotel - checkin = Ymd.Ymd(query['checkin']) - checkout = Ymd.Ymd(query['checkout']) - total_days = checkout-checkin - #get available rooms and total according to dates and guests - filters = { - 'room_type__max_guests__gte':query['guests'] - } - exclude = { - 'booking__checkin__lte':query['checkout'], - 'booking__checkout__gte':query['checkin'], - 'booking__state__exact':"NEW" - } - rooms = (Room.objects - .filter(**filters) - .exclude(**exclude) - .annotate(total = total_days*F('room_type__price')) - .order_by("room_type__max_guests","name") - ) - total_rooms = (Room.objects - .filter(**filters) - .values("room_type__name","room_type") - .exclude(**exclude) - .annotate(total = Count('room_type')) - .order_by("room_type__max_guests")) - #prepare context data for template - data = { - 'total_days':total_days - } - #pass the actual url query to the template - url_query = request.POST.urlencode() - context = { - "rooms":rooms, - "total_rooms":total_rooms, - "query":query, - "url_query":url_query, - "data":data - } - return render(request,"search.html",context) - -class HomeView(View): - #renders home page with all the bookingings order by date of creation - def get(self,request): - bookings = Booking.objects.all().order_by("-created") - context = { - 'bookings':bookings - } - return render(request,"home.html",context) - -class BookingView(View): - @method_decorator(ensure_csrf_cookie) - def post(self, request,pk): - #check if customer form is ok - customer_form = CustomerForm(request.POST,prefix = "customer") - if customer_form.is_valid(): - #save customer data - customer = customer_form.save() - #add the customer id to the booking form - temp_POST = request.POST.copy() - temp_POST.update({ - 'booking-customer':customer.id, - 'booking-room':pk, - 'booking-code':generate.get()}) - #if ok, save booking data - booking_form = BookingForm(temp_POST,prefix = "booking") - if booking_form.is_valid(): - booking_form.save() - return redirect('/') - - def get(self,request,pk): - #renders the form for booking confirmation. - # It returns 2 forms, the one with the booking info is hidden - #The second form is for the customer information - - query = request.GET.dict() - room = Room.objects.get(id = pk) - checkin = Ymd.Ymd(query['checkin']) - checkout = Ymd.Ymd(query['checkout']) - total_days = checkout-checkin - total = total_days*room.room_type.price #total amount to be paid - query['total'] = total - url_query = request.GET.urlencode() - booking_form = BookingFormExcluded(prefix = "booking",initial = query) - customer_form = CustomerForm(prefix = "customer") - context = { - "url_query":url_query, - "room":room, - "booking_form":booking_form, - "customer_form":customer_form - } - return render(request,"booking.html",context) - -class DeleteBookingView(View): - #renders the booking deletion form - def get(self,request,pk): - - booking = Booking.objects.get(id=pk) - context = { - 'booking':booking - } - return render(request,"delete_booking.html",context) - - #deletes the booking - def post(self,request,pk): - Booking.objects.filter(id=pk).update(state="DEL") - return redirect("/") - - -class EditBookingView(View): - #renders the booking edition form - def get(self,request,pk): - - booking = Booking.objects.get(id=pk) - booking_form = BookingForm(prefix="booking",instance=booking) - customer_form = CustomerForm(prefix="customer",instance=booking.customer) - context = { - 'booking_form':booking_form, - 'customer_form':customer_form - - } - return render(request,"edit_booking.html",context) - - - #updates the customer form - @method_decorator(ensure_csrf_cookie) - def post(self,request,pk): - booking = Booking.objects.get(id=pk) - customer_form = CustomerForm(request.POST,prefix = "customer",instance=booking.customer) - if customer_form.is_valid(): - customer_form.save() - return redirect("/") - -class DashboardView(View): - def get(self, request): - context = { - 'dashboard': get_dashboard_data() - } - return render(request, "dashboard.html", context) - -class RoomDetailsView(View): - def get(self,request,pk): - #renders room details - room = Room.objects.get(id=pk) - bookings = room.booking_set.all() - context = { - 'room':room, - 'bookings':bookings} - print(context) - return render(request,"room_detail.html",context) - - -class RoomsView(View): - def get(self,request): - #renders a list of rooms - rooms = Room.objects.all().values("name","room_type__name","id") - context = { - 'rooms':rooms - } - return render(request,"rooms.html",context) \ No newline at end of file diff --git a/pms/views/__init__.py b/pms/views/__init__.py new file mode 100644 index 000000000..2c03dcf2e --- /dev/null +++ b/pms/views/__init__.py @@ -0,0 +1,11 @@ +from .booking import HomeView, BookingView, DeleteBookingView, EditBookingView, EditBookingDateView +from .search import BookingSearchView +from .room import RoomSearchView, RoomsView, RoomDetailsView +from .dashboard import DashboardView + +__all__ = [ + 'BookingView', 'DeleteBookingView', 'EditBookingView', 'EditBookingDateView', + 'BookingSearchView', + 'RoomSearchView', 'RoomsView', 'RoomDetailsView', + 'HomeView', 'DashboardView', +] diff --git a/pms/views/booking.py b/pms/views/booking.py new file mode 100644 index 000000000..c85027708 --- /dev/null +++ b/pms/views/booking.py @@ -0,0 +1,122 @@ +from django.shortcuts import render, redirect +from django.views import View +from django.db import transaction +from ..models import Room, Booking +from ..forms import CustomerForm, BookingForm, BookingFormExcluded, BookingDateForm +from ..form_dates import Ymd +from django.views.decorators.csrf import ensure_csrf_cookie +from django.utils.decorators import method_decorator +from ..reservation_code import generate +from ..services.booking_service import is_room_available + + +class HomeView(View): + def get(self, request): + bookings = Booking.objects.all().order_by("-created") + context = {'bookings': bookings} + return render(request, "home.html", context) + + +class BookingView(View): + @method_decorator(ensure_csrf_cookie) + def post(self, request, pk): + customer_form = CustomerForm(request.POST, prefix="customer") + if customer_form.is_valid(): + customer = customer_form.save() + temp_POST = request.POST.copy() + temp_POST.update({ + 'booking-customer': customer.id, + 'booking-room': pk, + 'booking-code': generate.get() + }) + booking_form = BookingForm(temp_POST, prefix="booking") + if booking_form.is_valid(): + booking_form.save() + return redirect('/') + + def get(self, request, pk): + query = request.GET.dict() + room = Room.objects.get(id=pk) + checkin = Ymd.Ymd(query['checkin']) + checkout = Ymd.Ymd(query['checkout']) + total_days = checkout - checkin + total = total_days * room.room_type.price + query['total'] = total + url_query = request.GET.urlencode() + booking_form = BookingFormExcluded(prefix="booking", initial=query) + customer_form = CustomerForm(prefix="customer") + context = { + "url_query": url_query, + "room": room, + "booking_form": booking_form, + "customer_form": customer_form + } + return render(request, "booking.html", context) + + +class DeleteBookingView(View): + def get(self, request, pk): + booking = Booking.objects.get(id=pk) + context = {'booking': booking} + return render(request, "delete_booking.html", context) + + def post(self, request, pk): + Booking.objects.filter(id=pk).update(state="DEL") + return redirect("/") + + +class EditBookingView(View): + def get(self, request, pk): + booking = Booking.objects.get(id=pk) + booking_form = BookingForm(prefix="booking", instance=booking) + customer_form = CustomerForm(prefix="customer", instance=booking.customer) + context = { + 'booking_form': booking_form, + 'customer_form': customer_form + } + return render(request, "edit_booking.html", context) + + @method_decorator(ensure_csrf_cookie) + def post(self, request, pk): + booking = Booking.objects.get(id=pk) + customer_form = CustomerForm(request.POST, prefix="customer", instance=booking.customer) + if customer_form.is_valid(): + customer_form.save() + return redirect("/") + + +class EditBookingDateView(View): + def _render_form(self, booking, date_form): + """Helper para renderizar el formulario con contexto consistente.""" + context = { + 'booking': booking, + 'date_form': date_form + } + return render(self.request, "edit_booking_date.html", context) + + def get(self, request, pk): + self.request = request + booking = Booking.objects.get(id=pk) + date_form = BookingDateForm(instance=booking) + return self._render_form(booking, date_form) + + def post(self, request, pk): + self.request = request + with transaction.atomic(): + # Lock la reserva para evitar condiciones de carrera + booking = Booking.objects.select_for_update().get(id=pk) + date_form = BookingDateForm(request.POST, instance=booking) + + if date_form.is_valid(): + new_checkin = date_form.cleaned_data['checkin'] + new_checkout = date_form.cleaned_data['checkout'] + + # Validar disponibilidad dentro del lock (seguro en concurrencia) + if not is_room_available(booking.room, new_checkin, new_checkout, exclude_booking_id=booking.id): + date_form.add_error(None, "No hay disponibilidad para las fechas seleccionadas para esta habitación.") + return self._render_form(booking, date_form) + + date_form.save() + return redirect('/') + + return self._render_form(booking, date_form) diff --git a/pms/views/dashboard.py b/pms/views/dashboard.py new file mode 100644 index 000000000..a0cd4ce58 --- /dev/null +++ b/pms/views/dashboard.py @@ -0,0 +1,8 @@ +from django.shortcuts import render +from django.views import View +from ..services.dashboard_service import get_dashboard_data + +class DashboardView(View): + def get(self, request): + context = {'dashboard': get_dashboard_data()} + return render(request, "dashboard.html", context) diff --git a/pms/views/room.py b/pms/views/room.py new file mode 100644 index 000000000..0d9360af7 --- /dev/null +++ b/pms/views/room.py @@ -0,0 +1,66 @@ +from django.shortcuts import render +from django.views import View +from django.db.models import F, Count +from ..models import Room +from ..forms import RoomSearchForm +from ..form_dates import Ymd + + +class RoomSearchView(View): + def get(self, request): + room_search_form = RoomSearchForm() + context = {'form': room_search_form} + return render(request, "booking_search_form.html", context) + + def post(self, request): + query = request.POST.dict() + checkin = Ymd.Ymd(query['checkin']) + checkout = Ymd.Ymd(query['checkout']) + total_days = checkout - checkin + + filters = {'room_type__max_guests__gte': query['guests']} + exclude = { + 'booking__checkin__lte': query['checkout'], + 'booking__checkout__gte': query['checkin'], + 'booking__state__exact': "NEW" + } + + rooms = (Room.objects + .filter(**filters) + .exclude(**exclude) + .annotate(total=total_days*F('room_type__price')) + .order_by("room_type__max_guests", "name") + ) + + total_rooms = (Room.objects + .filter(**filters) + .values("room_type__name", "room_type") + .exclude(**exclude) + .annotate(total=Count('room_type')) + .order_by("room_type__max_guests")) + + data = {'total_days': total_days} + url_query = request.POST.urlencode() + context = { + "rooms": rooms, + "total_rooms": total_rooms, + "query": query, + "url_query": url_query, + "data": data + } + return render(request, "search.html", context) + + +class RoomsView(View): + def get(self, request): + rooms = Room.objects.all().values("name", "room_type__name", "id") + context = {'rooms': rooms} + return render(request, "rooms.html", context) + + +class RoomDetailsView(View): + def get(self, request, pk): + room = Room.objects.get(id=pk) + bookings = room.booking_set.all() + context = {'room': room, 'bookings': bookings} + return render(request, "room_detail.html", context) diff --git a/pms/views/search.py b/pms/views/search.py new file mode 100644 index 000000000..dae6417e6 --- /dev/null +++ b/pms/views/search.py @@ -0,0 +1,22 @@ +from django.shortcuts import render, redirect +from django.views import View +from django.db.models import Q +from ..models import Booking +from ..forms import RoomSearchForm + + +class BookingSearchView(View): + def get(self, request): + query = request.GET.dict() + if "filter" not in query: + return redirect("/") + bookings = (Booking.objects + .filter(Q(code__icontains=query['filter']) | Q(customer__name__icontains=query['filter'])) + .order_by("-created")) + room_search_form = RoomSearchForm() + context = { + 'bookings': bookings, + 'form': room_search_form, + 'filter': True + } + return render(request, "home.html", context)