diff --git a/pms/templates/dashboard.html b/pms/templates/dashboard.html index 10f0285cc..e331c78ee 100644 --- a/pms/templates/dashboard.html +++ b/pms/templates/dashboard.html @@ -17,11 +17,14 @@
+ {% if rooms %} + {{ rooms|length }} resultado{{ rooms|length|pluralize:"s" }} para "{{ query }}" + {% else %} + No se encontraron habitaciones para "{{ query }}". + {% endif %} +
+{% endif %} + +{% for room in rooms %}No hay habitaciones registradas.
+{% endif %} {% endfor %} -{% endblock content%} +{% endblock content %} + diff --git a/pms/tests.py b/pms/tests.py index 7ce503c2d..61b0b7728 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,3 +1,161 @@ -from django.test import TestCase +from django.test import TestCase, Client +from django.urls import reverse + +from .models import Booking, Customer, Room, Room_type + + +class RoomsViewSearchTest(TestCase): + """Tests for the room list search filter (GET ?q=).""" + + @classmethod + def setUpTestData(cls): + room_type = Room_type.objects.create(name="Estándar", price=80.0, max_guests=2) + Room.objects.create(name="Habitación 101", description="Vista al jardín", room_type=room_type) + Room.objects.create(name="Habitación 202", description="Vista a la piscina", room_type=room_type) + Room.objects.create(name="Suite Presidencial", description="Planta ática", room_type=room_type) + + def setUp(self): + self.client = Client() + self.url = reverse("rooms") + + # --- baseline --- + + def test_returns_200(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + + def test_no_query_returns_all_rooms(self): + response = self.client.get(self.url) + self.assertEqual(len(response.context["rooms"]), 3) + + def test_empty_query_string_returns_all_rooms(self): + response = self.client.get(self.url, {"q": ""}) + self.assertEqual(len(response.context["rooms"]), 3) + + def test_whitespace_only_query_returns_all_rooms(self): + """Strips whitespace so ' ' is treated as no query.""" + response = self.client.get(self.url, {"q": " "}) + self.assertEqual(len(response.context["rooms"]), 3) + + # --- filtering --- + + def test_search_returns_matching_rooms(self): + response = self.client.get(self.url, {"q": "Habitación"}) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 2) + self.assertTrue(all("Habitación" in r["name"] for r in rooms)) + + def test_search_partial_match(self): + response = self.client.get(self.url, {"q": "101"}) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 1) + self.assertEqual(rooms[0]["name"], "Habitación 101") + + def test_search_no_results(self): + response = self.client.get(self.url, {"q": "xyzabc"}) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 0) + + # --- case insensitivity --- + + def test_search_is_case_insensitive_lowercase(self): + response = self.client.get(self.url, {"q": "suite"}) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 1) + self.assertEqual(rooms[0]["name"], "Suite Presidencial") + + def test_search_is_case_insensitive_uppercase_ascii(self): + # SQLite's LIKE only case-folds ASCII characters; use an ASCII-safe term. + response = self.client.get(self.url, {"q": "SUITE"}) + rooms = list(response.context["rooms"]) + self.assertEqual(len(rooms), 1) + self.assertEqual(rooms[0]["name"], "Suite Presidencial") + + # --- context --- + + def test_query_term_preserved_in_context(self): + response = self.client.get(self.url, {"q": "Suite"}) + self.assertEqual(response.context["query"], "Suite") + + def test_empty_query_context_is_empty_string(self): + response = self.client.get(self.url) + self.assertEqual(response.context["query"], "") + + # --- ordering --- + + def test_results_ordered_by_name(self): + response = self.client.get(self.url) + names = [r["name"] for r in response.context["rooms"]] + self.assertEqual(names, sorted(names)) + + +class DashboardOccupancyTest(TestCase): + """Tests for the occupancy rate widget in the Dashboard.""" + + @classmethod + def setUpTestData(cls): + room_type = Room_type.objects.create(name="Estándar", price=100.0, max_guests=2) + customer = Customer.objects.create(name="Ana García", email="ana@example.com", phone="600000000") + + cls.room1 = Room.objects.create(name="Hab 101", description="", room_type=room_type) + cls.room2 = Room.objects.create(name="Hab 102", description="", room_type=room_type) + cls.room3 = Room.objects.create(name="Hab 103", description="", room_type=room_type) + cls.room4 = Room.objects.create(name="Hab 104", description="", room_type=room_type) + + # 3 active bookings (NEW), 1 cancelled (DEL) + for room in [cls.room1, cls.room2, cls.room3]: + Booking.objects.create( + checkin="2026-05-01", + checkout="2026-05-05", + room=room, + guests=1, + customer=customer, + total=400.0, + code=f"TST{room.id:05d}", + state=Booking.NEW, + ) + Booking.objects.create( + checkin="2026-05-01", + checkout="2026-05-05", + room=cls.room4, + guests=1, + customer=customer, + total=400.0, + code="TSTDEL01", + state=Booking.DELETED, + ) + + def setUp(self): + self.client = Client() + self.url = reverse("dashboard") + + def test_dashboard_returns_200(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + + def test_occupancy_rate_calculation(self): + # 3 NEW bookings / 4 rooms * 100 = 75% + response = self.client.get(self.url) + self.assertEqual(response.context["dashboard"]["occupancy_rate"], 75) + + def test_cancelled_bookings_excluded_from_occupancy(self): + # The DEL booking must not count towards occupancy + response = self.client.get(self.url) + self.assertLess(response.context["dashboard"]["occupancy_rate"], 100) + + def test_occupancy_rate_in_template(self): + response = self.client.get(self.url) + self.assertContains(response, "75%") + + def test_occupancy_zero_when_no_rooms(self): + # Temporarily wipe rooms to test division-by-zero guard + Room.objects.all().delete() + response = self.client.get(self.url) + self.assertEqual(response.context["dashboard"]["occupancy_rate"], 0) + + def test_occupancy_100_when_all_rooms_booked(self): + Booking.objects.filter(state=Booking.DELETED).update(state=Booking.NEW) + response = self.client.get(self.url) + self.assertEqual(response.context["dashboard"]["occupancy_rate"], 100) + -# Create your tests here. diff --git a/pms/views.py b/pms/views.py index f38563933..1a18859b6 100644 --- a/pms/views.py +++ b/pms/views.py @@ -1,3 +1,5 @@ +from datetime import date, datetime, time + from django.db.models import F, Q, Count, Sum from django.shortcuts import render, redirect from django.utils.decorators import method_decorator @@ -6,7 +8,7 @@ from .form_dates import Ymd from .forms import * -from .models import Room +from .models import Booking, Room from .reservation_code import generate @@ -176,52 +178,40 @@ def post(self, request, pk): class DashboardView(View): def get(self, request): - from datetime import date, time, datetime today = date.today() + today_range = (datetime.combine(today, time.min), datetime.combine(today, time.max)) + + bookings_today = Booking.objects.filter(created__range=today_range) - # 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() + new_bookings = bookings_today.count() - # get incoming guests incoming = (Booking.objects .filter(checkin=today) - .exclude(state="DEL") - .values("id") - ).count() + .exclude(state=Booking.DELETED) + .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 + .exclude(state=Booking.DELETED) + .count()) - } + invoiced = (bookings_today + .exclude(state=Booking.DELETED) + .aggregate(Sum("total"))) - context = { - 'dashboard': dashboard + total_rooms = Room.objects.count() + active_bookings = Booking.objects.filter(state=Booking.NEW).count() + occupancy_rate = round((active_bookings / total_rooms) * 100) if total_rooms > 0 else 0 + + dashboard = { + "new_bookings": new_bookings, + "incoming_guests": incoming, + "outcoming_guests": outcoming, + "invoiced": invoiced, + "occupancy_rate": occupancy_rate, } - return render(request, "dashboard.html", context) + + return render(request, "dashboard.html", {"dashboard": dashboard}) class RoomDetailsView(View): @@ -238,9 +228,16 @@ 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") + query = request.GET.get("q", "").strip() + rooms = Room.objects.select_related("room_type").values("name", "room_type__name", "id") + + if query: + rooms = rooms.filter(name__icontains=query) + + rooms = rooms.order_by("name") + context = { - 'rooms': rooms + "rooms": rooms, + "query": query, } return render(request, "rooms.html", context)