From bd2a75aa7db5fe275ff66e4cc1c87fc5a7faabd6 Mon Sep 17 00:00:00 2001 From: Daniel Socas Date: Wed, 1 Apr 2026 07:58:21 +0100 Subject: [PATCH 1/5] refactor(dashboard): simplify dashboard count queries - move datetime imports to module level - remove redundant .values("id") before .count() calls - replace hardcoded "DEL" with Booking.DELETED in touched queries --- pms/views.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/pms/views.py b/pms/views.py index f38563933..a2e071d02 100644 --- a/pms/views.py +++ b/pms/views.py @@ -1,3 +1,5 @@ +from datetime import date, time, datetime + from django.db.models import F, Q, Count, Sum from django.shortcuts import render, redirect from django.utils.decorators import method_decorator @@ -176,7 +178,6 @@ def post(self, request, pk): class DashboardView(View): def get(self, request): - from datetime import date, time, datetime today = date.today() # get bookings created today @@ -185,27 +186,27 @@ def get(self, request): today_range = (today_min, today_max) new_bookings = (Booking.objects .filter(created__range=today_range) - .values("id") - ).count() + .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() + .exclude(state=Booking.DELETED) + .count() + ) - # get outcoming guests + # get invoiced total invoiced = (Booking.objects .filter(created__range=today_range) - .exclude(state="DEL") + .exclude(state=Booking.DELETED) .aggregate(Sum('total')) ) @@ -214,8 +215,7 @@ def get(self, request): 'new_bookings': new_bookings, 'incoming_guests': incoming, 'outcoming_guests': outcoming, - 'invoiced': invoiced - + 'invoiced': invoiced, } context = { @@ -223,7 +223,6 @@ def get(self, request): } return render(request, "dashboard.html", context) - class RoomDetailsView(View): def get(self, request, pk): # renders room details From 90847455f1be7ef84d24e3d266a4609e12173676 Mon Sep 17 00:00:00 2001 From: Daniel Socas Date: Wed, 1 Apr 2026 08:13:29 +0100 Subject: [PATCH 2/5] fix(dashboard): exclude deleted bookings from new bookings count --- pms/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms/views.py b/pms/views.py index a2e071d02..421a21a29 100644 --- a/pms/views.py +++ b/pms/views.py @@ -186,6 +186,7 @@ def get(self, request): today_range = (today_min, today_max) new_bookings = (Booking.objects .filter(created__range=today_range) + .exclude(state=Booking.DELETED) .count() ) From 010f9a937014bae357ab1427a9c54e4cb8911ce1 Mon Sep 17 00:00:00 2001 From: Daniel Socas Date: Wed, 1 Apr 2026 08:18:35 +0100 Subject: [PATCH 3/5] feat(dashboard): add occupancy percentage metric - calculate as confirmed bookings / total rooms - round to one decimal place - guard against division by zero --- pms/views.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pms/views.py b/pms/views.py index 421a21a29..1bb6e4713 100644 --- a/pms/views.py +++ b/pms/views.py @@ -8,7 +8,7 @@ from .form_dates import Ymd from .forms import * -from .models import Room +from .models import Room, Booking from .reservation_code import generate @@ -211,12 +211,21 @@ def get(self, request): .aggregate(Sum('total')) ) + # get occupancy percentage + total_rooms = Room.objects.count() + confirmed_bookings = Booking.objects.filter(state=Booking.NEW).count() + occupancy_percentage = ( + round((confirmed_bookings / total_rooms) * 100, 1) + if total_rooms else 0 + ) + # preparing context data dashboard = { 'new_bookings': new_bookings, 'incoming_guests': incoming, 'outcoming_guests': outcoming, 'invoiced': invoiced, + 'occupancy_percentage': occupancy_percentage, } context = { From fc6e4dbbad1451426e5f1d93be812861b81e05ae Mon Sep 17 00:00:00 2001 From: Daniel Socas Date: Wed, 1 Apr 2026 08:38:50 +0100 Subject: [PATCH 4/5] feat(dashboard): display occupancy percentage widget --- pms/templates/dashboard.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pms/templates/dashboard.html b/pms/templates/dashboard.html index 10f0285cc..ca8940c8c 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}}%

+
-{% endblock content%} \ No newline at end of file +{% endblock content%} From 1d2719d8c4995345f1bfcba99b1fdba2fa6acfdd Mon Sep 17 00:00:00 2001 From: Daniel Socas Date: Wed, 1 Apr 2026 09:54:14 +0100 Subject: [PATCH 5/5] test(dashboard): add tests for occupancy percentage and deleted booking exclusion - cover widget rendering and occupancy calculation from active bookings - cover deleted booking exclusion from occupancy and new bookings count - cover zero-room edge case to prevent division by zero --- pms/tests.py | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/pms/tests.py b/pms/tests.py index 7ce503c2d..00e180eef 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,3 +1,102 @@ -from django.test import TestCase +from datetime import date -# Create your tests here. +from django.test import TestCase, override_settings +from django.urls import reverse + +from .models import Booking, Customer, Room, Room_type + + +@override_settings(STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage") +class DashboardViewTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.dashboard_url = reverse("dashboard") + + cls.room_type = Room_type.objects.create( + name="Double", + price=30, + max_guests=2, + ) + + cls.room_one = Room.objects.create( + room_type=cls.room_type, + name="Room 1.1", + ) + cls.room_two = Room.objects.create( + room_type=cls.room_type, + name="Room 1.2", + ) + cls.room_three = Room.objects.create( + room_type=cls.room_type, + name="Room 1.3", + ) + + cls.customer = Customer.objects.create( + name="John Doe", + email="john@example.com", + phone="123456789", + ) + + def create_booking(self, room, state=Booking.NEW, total=60, code=None): + return Booking.objects.create( + state=state, + checkin=date.today(), + checkout=date.today(), + room=room, + guests=1, + customer=self.customer, + total=total, + code=code or f"CODE{Booking.objects.count() + 1:04d}", + ) + + def test_dashboard_renders_occupancy_widget(self): + response = self.client.get(self.dashboard_url) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "% Ocupación") + self.assertEqual(response.context["dashboard"]["occupancy_percentage"], 0) + + def test_dashboard_calculates_occupancy_percentage_from_active_bookings(self): + self.create_booking(room=self.room_one, state=Booking.NEW) + self.create_booking(room=self.room_two, state=Booking.NEW) + + response = self.client.get(self.dashboard_url) + + self.assertEqual(response.status_code, 200) + self.assertAlmostEqual( + response.context["dashboard"]["occupancy_percentage"], + 66.7, + places=1, + ) + self.assertContains(response, "66.7%") + + def test_dashboard_excludes_deleted_bookings_from_occupancy_percentage(self): + self.create_booking(room=self.room_one, state=Booking.NEW) + self.create_booking(room=self.room_two, state=Booking.DELETED) + + response = self.client.get(self.dashboard_url) + + self.assertEqual(response.status_code, 200) + self.assertAlmostEqual( + response.context["dashboard"]["occupancy_percentage"], + 33.3, + places=1, + ) + self.assertContains(response, "33.3%") + + def test_dashboard_excludes_deleted_bookings_from_new_bookings_count(self): + self.create_booking(room=self.room_one, state=Booking.NEW) + self.create_booking(room=self.room_two, state=Booking.DELETED) + + response = self.client.get(self.dashboard_url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["dashboard"]["new_bookings"], 1) + + def test_dashboard_sets_zero_occupancy_when_there_are_no_rooms(self): + Room.objects.all().delete() + + response = self.client.get(self.dashboard_url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["dashboard"]["occupancy_percentage"], 0)