Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pms/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ <h1 class="dashboard-value">{{dashboard.incoming_guests}}</h1>
<h5 class="small">Huéspedes saliendo</h5>
<h1 class="dashboard-value">{{dashboard.outcoming_guests}}</h1>
</div>

<div class="card text-white p-3 card-customization" style="background-color: #ff7f7f;">
<h5 class="small">Total facturado</h5>
<h1 class="dashboard-value">€ {% if dashboard.invoiced.total__sum == None %}0.00{% endif %} {{dashboard.invoiced.total__sum|floatformat:2}}</h1>
</div>
<div class="card text-white p-3 card-customization" style="background-color: #7b2d8b;">
<h5 class="small">Ocupación</h5>
<h1 class="dashboard-value">{{ dashboard.occupancy_rate }}%</h1>
</div>
</div>
</div>
{% endblock content%}
52 changes: 43 additions & 9 deletions pms/templates/rooms.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,53 @@
{% extends "main.html"%}

{% block content %}
<h1>Habitaciones del hotel</h1>
{% for room in rooms%}
<div class="d-flex justify-content-between align-items-center mt-4 mb-3">
<h1 class="mb-0">Habitaciones del hotel</h1>
</div>

<form method="GET" action="" class="mb-4">
<div class="input-group">
<input
type="search"
name="q"
class="form-control"
placeholder="Buscar por nombre de habitación…"
value="{{ query }}"
aria-label="Buscar habitación"
autocomplete="off"
>
<button class="btn btn-primary" type="submit">
<i class="bi bi-search me-1"></i> Buscar
</button>
{% if query %}
<a href="{% url 'rooms' %}" class="btn btn-outline-secondary">Limpiar</a>
{% endif %}
</div>
</form>

{% if query %}
<p class="text-muted small mb-3">
{% if rooms %}
{{ rooms|length }} resultado{{ rooms|length|pluralize:"s" }} para "<strong>{{ query }}</strong>"
{% else %}
No se encontraron habitaciones para "<strong>{{ query }}</strong>".
{% endif %}
</p>
{% endif %}

{% for room in rooms %}
<div class="row card mt-3 mb-3 hover-card bg-tr-250">
<div class="col p-3">
<div class="">
{{room.name}} ({{room.room_type__name}})
</div>
<div>{{ room.name }} ({{ room.room_type__name }})</div>
<div>
<a href="{% url 'room_details' pk=room.id%}">Ver detalles</a>
<a href="{% url 'room_details' pk=room.id %}">Ver detalles</a>
</div>

</div>

</div>
{% empty %}
{% if not query %}
<p class="text-muted mt-4">No hay habitaciones registradas.</p>
{% endif %}
{% endfor %}
{% endblock content%}
{% endblock content %}

162 changes: 160 additions & 2 deletions pms/tests.py
Original file line number Diff line number Diff line change
@@ -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.
75 changes: 36 additions & 39 deletions pms/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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):
Expand All @@ -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)