diff --git a/pms/forms.py b/pms/forms.py index f1bc68d08..21ed3709d 100644 --- a/pms/forms.py +++ b/pms/forms.py @@ -56,3 +56,39 @@ class Meta: 'total': forms.HiddenInput(), 'state': forms.HiddenInput(), } + +class EditBookingDateForm(ModelForm): + class Meta: + model = Booking + fields = ['checkin', 'checkout'] + widgets = { + 'checkin': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + 'checkout': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + } + + def clean(self): + cleaned_data = super().clean() + checkin = cleaned_data.get("checkin") + checkout = cleaned_data.get("checkout") + + if checkin and checkout: + # Basic validation + if checkin >= checkout: + raise forms.ValidationError("La fecha de salida debe ser posterior a la de entrada.") + + # Check availability + overlapping_bookings = Booking.objects.filter( + room=self.instance.room, + checkin__lt=checkout, + checkout__gt=checkin + ).exclude( + id=self.instance.id # exclude same reservation + ).exclude( + state='DEL' + ) + + if overlapping_bookings.exists(): + + raise forms.ValidationError("No hay disponibilidad para las fechas seleccionadas") + + return cleaned_data \ No newline at end of file diff --git a/pms/templates/edit_booking_date.html b/pms/templates/edit_booking_date.html new file mode 100644 index 000000000..853cca904 --- /dev/null +++ b/pms/templates/edit_booking_date.html @@ -0,0 +1,44 @@ +{% extends "main.html" %} + +{% block content %} +
+

Editar Fechas de la Reserva: {{ booking.code }}

+
+ +
+
+
+

Habitación: {{ booking.room.name }}

+

Fechas actuales: {{ booking.checkin }} a {{ booking.checkout }}

+ +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + +
+ + {{ form.checkin }} + {{ form.checkin.errors }} +
+ +
+ + {{ form.checkout }} + {{ form.checkout.errors }} +
+ + Cancelar + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/pms/templates/home.html b/pms/templates/home.html index 1e61b8024..ed46123e0 100644 --- a/pms/templates/home.html +++ b/pms/templates/home.html @@ -65,15 +65,16 @@

Reservas Realizadas

- Editar datos de contacto + Editar datos de contacto
-
+
{% if booking.state != "DEL" %} - Cancelar reserva + Editar Fecha de la Reserva + Cancelar reserva {% endif %}
diff --git a/pms/tests.py b/pms/tests.py index 7ce503c2d..141c18764 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,3 +1,88 @@ from django.test import TestCase +from django.utils import timezone +from datetime import timedelta +from .models import Room, Room_type, Customer, Booking +from .forms import EditBookingDateForm -# Create your tests here. +class EditBookingDateFormUnitTests(TestCase): + def setUp(self): + # Date today + self.today = timezone.now().date() + + # Setup data + self.room_type = Room_type.objects.create(name="Doble", price=50.0, max_guests=2) + self.room = Room.objects.create(name="101", room_type=self.room_type) + self.customer = Customer.objects.create(name="Test", email="test@test.com", phone="123") + + # Reservation A: stays today & tomorrow + self.booking_a = Booking.objects.create( + room=self.room, customer=self.customer, + checkin=self.today, checkout=self.today + timedelta(days=2), + guests=1, total=100.0, code="BKA", state="NEW" + ) + + # Reservation B stays from day 5 to day 10 + self.booking_b = Booking.objects.create( + room=self.room, customer=self.customer, + checkin=self.today + timedelta(days=5), checkout=self.today + timedelta(days=10), + guests=1, total=250.0, code="BKB", state="NEW" + ) + + def test_form_valid_dates(self): + """ + Case: book in week available + expect: successfully changed + """ + data = { + 'checkin': self.today + timedelta(days=20), + 'checkout': self.today + timedelta(days=25) + } + + form = EditBookingDateForm(data=data, instance=self.booking_a) + + self.assertTrue(form.is_valid()) + + def test_form_invalid_checkout_before_checkin(self): + """ + Case: try to checkout before of checkin + expect: invalid form & feedback error message + """ + error_text = "La fecha de salida debe ser posterior a la de entrada." + data = { + 'checkin': self.today + timedelta(days=5), + 'checkout': self.today + timedelta(days=3) # Checkout before of checkin + } + form = EditBookingDateForm(data=data, instance=self.booking_a) + + + self.assertFalse(form.is_valid()) + # Comprobamos que el error es exactamente el que programamos + self.assertIn(error_text, form.errors['__all__']) + + def test_form_overlap_with_other_booking(self): + """ + CASE: overlapping with reservation B + Expect: invalid form & feedback error message + """ + error_text = "No hay disponibilidad para las fechas seleccionadas" + data = { + 'checkin': self.today + timedelta(days=6), # Entra en medio de la Reserva B + 'checkout': self.today + timedelta(days=8) + } + form = EditBookingDateForm(data=data, instance=self.booking_a) + + self.assertFalse(form.is_valid()) + self.assertIn(error_text, form.errors['__all__']) + + def test_form_self_exclusion(self): + """ + Case: Submitting the form with the booking's currently saved dates + Expect : valid form + """ + data = { + 'checkin': self.booking_a.checkin, + 'checkout': self.booking_a.checkout + } + form = EditBookingDateForm(data=data, instance=self.booking_a) + + self.assertTrue(form.is_valid()) \ No newline at end of file diff --git a/pms/urls.py b/pms/urls.py index c18714abf..e905ca58e 100644 --- a/pms/urls.py +++ b/pms/urls.py @@ -7,7 +7,8 @@ path("search/room/", views.RoomSearchView.as_view(), name="search"), 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/contact/", views.EditBookingView.as_view(), name="edit_booking_contact"), + path("booking//edit/date/", views.EditBookingDateView.as_view(), name="edit_booking_date"), 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..559c1e4b7 100644 --- a/pms/views.py +++ b/pms/views.py @@ -1,5 +1,5 @@ from django.db.models import F, Q, Count, Sum -from django.shortcuts import render, redirect +from django.shortcuts import render, redirect, get_object_or_404 from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import ensure_csrf_cookie @@ -244,3 +244,32 @@ def get(self, request): 'rooms': rooms } return render(request, "rooms.html", context) + + +class EditBookingDateView(View): + # renders the booking date edition form + def get(self, request, pk): + booking = get_object_or_404(Booking, pk=pk) + form = EditBookingDateForm(instance=booking) + + context = { + 'form': form, + 'booking': booking + } + return render(request, 'edit_booking_date.html', context) + + # processes the form and updates the dates + def post(self, request, pk): + booking = get_object_or_404(Booking, pk=pk) + form = EditBookingDateForm(request.POST, instance=booking) + + if form.is_valid(): + form.save() + return redirect('/') + + # re-render if form data is invalid + context = { + 'form': form, + 'booking': booking + } + return render(request, 'edit_booking_date.html', context)