diff --git a/pms/forms.py b/pms/forms.py index f1bc68d08..c7aadf703 100644 --- a/pms/forms.py +++ b/pms/forms.py @@ -56,3 +56,41 @@ class Meta: 'total': forms.HiddenInput(), 'state': forms.HiddenInput(), } + + +class BookingDatesForm(forms.Form): + checkin = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'})) + checkout = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'})) + + def __init__(self, *args, booking=None, **kwargs): + super().__init__(*args, **kwargs) + self.booking = booking + if self.booking is None: + raise ValueError("BookingDatesForm requires a booking instance") + + def clean(self): + cleaned_data = super().clean() + checkin = cleaned_data.get("checkin") + checkout = cleaned_data.get("checkout") + + if not checkin or not checkout: + return cleaned_data + + if checkout <= checkin: + raise forms.ValidationError( + "La fecha de salida debe ser posterior a la de entrada" + ) + + conflicts = Booking.objects.filter( + room=self.booking.room, + state=Booking.NEW, + checkin__lte=checkout, + checkout__gte=checkin, + ).exclude(pk=self.booking.pk) + + if conflicts.exists(): + raise forms.ValidationError( + "No hay disponibilidad para las fechas seleccionadas" + ) + + return cleaned_data diff --git a/pms/templates/edit_booking_dates.html b/pms/templates/edit_booking_dates.html new file mode 100644 index 000000000..9e6755238 --- /dev/null +++ b/pms/templates/edit_booking_dates.html @@ -0,0 +1,24 @@ +{% extends "main.html" %} + +{% block content %} +

Editar fechas de reserva

+ +
+ {% csrf_token %} + {{ form.non_field_errors }} + +
+ {{ form.checkin.label_tag }} + {{ form.checkin }} + {{ form.checkin.errors }} +
+ +
+ {{ form.checkout.label_tag }} + {{ form.checkout }} + {{ form.checkout.errors }} +
+ + +
+{% endblock content %} diff --git a/pms/templates/home.html b/pms/templates/home.html index 1e61b8024..17089cbef 100644 --- a/pms/templates/home.html +++ b/pms/templates/home.html @@ -64,8 +64,8 @@

Reservas Realizadas

- - Editar datos de contacto + Editar datos de contacto
+ Editar fechas
@@ -73,7 +73,7 @@

Reservas Realizadas

{% if booking.state != "DEL" %} - Cancelar reserva + Cancelar reserva {% endif %}
diff --git a/pms/tests.py b/pms/tests.py index 7ce503c2d..861844b46 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,3 +1,149 @@ -from django.test import TestCase +from datetime import date, timedelta -# 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 EditBookingDatesViewTests(TestCase): + def setUp(self): + self.today = date.today() + + self.room_type = Room_type.objects.create( + name="Doble", + price=30, + max_guests=2, + ) + self.room = Room.objects.create( + room_type=self.room_type, + name="Room 1.1", + description="Test room", + ) + self.customer = Customer.objects.create( + name="Test User", + email="test@example.com", + phone="123456789", + ) + self.booking = Booking.objects.create( + room=self.room, + customer=self.customer, + checkin=self.today, + checkout=self.today + timedelta(days=2), + guests=2, + total=60, + code="ABC12345", + state=Booking.NEW, + ) + self.url = reverse("edit_booking_dates", args=[self.booking.id]) + + def test_edit_dates_page_renders_with_initial_data(self): + response = self.client.get(self.url) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, str(self.booking.checkin)) + self.assertContains(response, str(self.booking.checkout)) + + def test_valid_date_update_updates_booking_dates(self): + new_checkin = self.today + timedelta(days=5) + new_checkout = new_checkin + timedelta(days=2) + + response = self.client.post(self.url, { + "checkin": new_checkin, + "checkout": new_checkout, + }) + + self.assertEqual(response.status_code, 302) + self.booking.refresh_from_db() + self.assertEqual(self.booking.checkin, new_checkin) + self.assertEqual(self.booking.checkout, new_checkout) + + def test_valid_date_update_recalculates_total(self): + new_checkin = self.today + timedelta(days=10) + new_checkout = new_checkin + timedelta(days=3) + + response = self.client.post(self.url, { + "checkin": new_checkin, + "checkout": new_checkout, + }) + + self.assertEqual(response.status_code, 302) + self.booking.refresh_from_db() + self.assertEqual(self.booking.total, 3 * self.room_type.price) + + def test_booking_does_not_conflict_with_itself(self): + response = self.client.post(self.url, { + "checkin": self.booking.checkin, + "checkout": self.booking.checkout, + }) + + self.assertEqual(response.status_code, 302) + + def test_overlapping_new_booking_blocks_date_update(self): + Booking.objects.create( + room=self.room, + customer=self.customer, + checkin=self.today + timedelta(days=3), + checkout=self.today + timedelta(days=6), + guests=2, + total=90, + code="XYZ12345", + state=Booking.NEW, + ) + + response = self.client.post(self.url, { + "checkin": self.today + timedelta(days=4), + "checkout": self.today + timedelta(days=7), + }) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "No hay disponibilidad para las fechas seleccionadas") + self.booking.refresh_from_db() + self.assertEqual(self.booking.checkin, self.today) + self.assertEqual(self.booking.checkout, self.today + timedelta(days=2)) + + def test_deleted_booking_does_not_block_date_update(self): + new_checkin = self.today + timedelta(days=4) + new_checkout = self.today + timedelta(days=7) + + Booking.objects.create( + room=self.room, + customer=self.customer, + checkin=self.today + timedelta(days=3), + checkout=self.today + timedelta(days=6), + guests=2, + total=90, + code="DEL12345", + state=Booking.DELETED, + ) + + response = self.client.post(self.url, { + "checkin": new_checkin, + "checkout": new_checkout, + }) + + self.assertEqual(response.status_code, 302) + self.booking.refresh_from_db() + self.assertEqual(self.booking.checkin, new_checkin) + self.assertEqual(self.booking.checkout, new_checkout) + + def test_adjacent_dates_follow_same_overlap_rule_as_create(self): + Booking.objects.create( + room=self.room, + customer=self.customer, + checkin=self.today + timedelta(days=3), + checkout=self.today + timedelta(days=6), + guests=2, + total=90, + code="ADJ12345", + state=Booking.NEW, + ) + + response = self.client.post(self.url, { + "checkin": self.today + timedelta(days=6), + "checkout": self.today + timedelta(days=8), + }) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "No hay disponibilidad para las fechas seleccionadas") diff --git a/pms/urls.py b/pms/urls.py index c18714abf..2acbd0a22 100644 --- a/pms/urls.py +++ b/pms/urls.py @@ -8,6 +8,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.EditBookingDatesView.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 index f38563933..a431c1774 100644 --- a/pms/views.py +++ b/pms/views.py @@ -174,6 +174,46 @@ def post(self, request, pk): return redirect("/") +class EditBookingDatesView(View): + def get(self, request, pk): + booking = Booking.objects.get(id=pk) + form = BookingDatesForm( + booking=booking, + initial={ + "checkin": booking.checkin, + "checkout": booking.checkout, + }, + ) + context = { + "booking": booking, + "form": form, + } + return render(request, "edit_booking_dates.html", context) + + @method_decorator(ensure_csrf_cookie) + def post(self, request, pk): + booking = Booking.objects.get(id=pk) + form = BookingDatesForm(request.POST, booking=booking) + + if form.is_valid(): + checkin = form.cleaned_data["checkin"] + checkout = form.cleaned_data["checkout"] + total_days = (checkout - checkin).days + + booking.checkin = checkin + booking.checkout = checkout + booking.total = total_days * booking.room.room_type.price + booking.save() + + return redirect("/") + + context = { + "booking": booking, + "form": form, + } + return render(request, "edit_booking_dates.html", context) + + class DashboardView(View): def get(self, request): from datetime import date, time, datetime