diff --git a/docs/pr-2-dashboard-occupancy.md b/docs/pr-2-dashboard-occupancy.md
new file mode 100644
index 000000000..9a40bb013
--- /dev/null
+++ b/docs/pr-2-dashboard-occupancy.md
@@ -0,0 +1,40 @@
+# PR 2 - Dashboard Occupancy
+
+## Objetivo de negocio
+Incorporar un indicador operativo de ocupación para facilitar seguimiento diario del rendimiento comercial y de capacidad del hotel desde el dashboard principal.
+
+## Alcance implementado
+- Se agregó un nuevo widget `% ocupación` en Dashboard.
+- El cálculo implementado sigue la regla definida para la prueba:
+ - **reservas confirmadas / total de habitaciones**.
+- Se contempló el caso borde sin habitaciones disponibles para evitar errores por división por cero.
+
+## Cambios funcionales y técnicos
+1. **Backend (cálculo de métrica)**
+- En `DashboardView` se añadió:
+ - conteo de reservas confirmadas (`state = NEW`),
+ - conteo total de habitaciones,
+ - cálculo de porcentaje de ocupación.
+- Se expone el valor como `dashboard.occupancy_percentage` para consumo en template.
+
+2. **Frontend (visualización)**
+- Se agregó un quinto widget en la grilla de Dashboard para mostrar `% ocupación`.
+- Se mantuvo la estructura de visualización existente y se integró el nuevo indicador en la misma jerarquía de información.
+
+3. **Calidad**
+- Se añadieron pruebas para validar:
+ - cálculo correcto en escenario estándar,
+ - resultado `0` cuando no hay habitaciones.
+
+## Archivos modificados
+- `pms/views.py`
+- `pms/templates/dashboard.html`
+- `pms/tests.py`
+
+## Pruebas ejecutadas
+- `python manage.py test pms.tests.DashboardOccupancyTests pms.tests.DashboardOccupancyWithoutRoomsTests`
+
+## Resultado esperado para el producto
+- Mayor visibilidad de capacidad utilizada.
+- Señal temprana para decisiones de pricing, promociones o gestión de disponibilidad.
+- Mejora del valor analítico del Dashboard sin incrementar complejidad operativa para el usuario final.
diff --git a/pms/templates/dashboard.html b/pms/templates/dashboard.html
index 10f0285cc..7acf779bc 100644
--- a/pms/templates/dashboard.html
+++ b/pms/templates/dashboard.html
@@ -22,6 +22,10 @@
{{dashboard.outcoming_guests}}
Total facturado
€ {% if dashboard.invoiced.total__sum == None %}0.00{% endif %} {{dashboard.invoiced.total__sum|floatformat:2}}
+
+
% ocupación
+ {{dashboard.occupancy_percentage|floatformat:2}}%
+
-{% endblock content%}
\ No newline at end of file
+{% endblock content%}
diff --git a/pms/tests.py b/pms/tests.py
index 7ce503c2d..00b4c9fe9 100644
--- a/pms/tests.py
+++ b/pms/tests.py
@@ -1,3 +1,52 @@
+from datetime import date, timedelta
+
from django.test import TestCase
+from django.test.utils import override_settings
+from django.urls import reverse
+
+from .models import Booking, Room, Room_type
+
+
+@override_settings(STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage")
+class DashboardOccupancyTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ room_type = Room_type.objects.create(name="Simple", price=20, max_guests=1)
+ room_1 = Room.objects.create(room_type=room_type, name="Room 1.1", description="Desc")
+ room_2 = Room.objects.create(room_type=room_type, name="Room 1.2", description="Desc")
+ today = date.today()
+
+ Booking.objects.create(
+ state="NEW",
+ checkin=today,
+ checkout=today + timedelta(days=1),
+ room=room_1,
+ guests=1,
+ total=20,
+ code="CONF0001",
+ )
+ Booking.objects.create(
+ state="DEL",
+ checkin=today,
+ checkout=today + timedelta(days=1),
+ room=room_2,
+ guests=1,
+ total=20,
+ code="CANC0001",
+ )
+
+ def test_dashboard_displays_occupancy_percentage_widget(self):
+ response = self.client.get(reverse("dashboard"))
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "% ocupación")
+ self.assertContains(response, "50.00%")
+ self.assertEqual(response.context["dashboard"]["occupancy_percentage"], 50.0)
+
-# Create your tests here.
+@override_settings(STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage")
+class DashboardOccupancyWithoutRoomsTests(TestCase):
+ def test_dashboard_occupancy_is_zero_when_no_rooms_exist(self):
+ response = self.client.get(reverse("dashboard"))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context["dashboard"]["occupancy_percentage"], 0)
+ self.assertContains(response, "0.00%")
diff --git a/pms/views.py b/pms/views.py
index f38563933..e5d01ceb8 100644
--- a/pms/views.py
+++ b/pms/views.py
@@ -208,13 +208,22 @@ def get(self, request):
.exclude(state="DEL")
.aggregate(Sum('total'))
)
+ confirmed_bookings = (Booking.objects
+ .filter(state="NEW")
+ .values("id")
+ ).count()
+ total_rooms = Room.objects.values("id").count()
+ occupancy_percentage = 0
+ if total_rooms > 0:
+ occupancy_percentage = (confirmed_bookings / total_rooms) * 100
# preparing context data
dashboard = {
'new_bookings': new_bookings,
'incoming_guests': incoming,
'outcoming_guests': outcoming,
- 'invoiced': invoiced
+ 'invoiced': invoiced,
+ 'occupancy_percentage': occupancy_percentage,
}