diff --git a/pms/forms.py b/pms/forms.py index f1bc68d08..4452a3372 100644 --- a/pms/forms.py +++ b/pms/forms.py @@ -56,3 +56,25 @@ class Meta: 'total': forms.HiddenInput(), 'state': forms.HiddenInput(), } + + +class EditBookingDatesForm(ModelForm): + class Meta: + model = Booking + fields = ['checkin', 'checkout'] + labels = { + 'checkin': 'Fecha de entrada', + 'checkout': 'Fecha de salida' + } + widgets = { + 'checkin': forms.DateInput(attrs={ + 'type': 'date', + 'class': 'form-control', + 'min': datetime.today().strftime('%Y-%m-%d') + }), + 'checkout': forms.DateInput(attrs={ + 'type': 'date', + 'class': 'form-control', + 'max': datetime.today().replace(month=12, day=31).strftime('%Y-%m-%d') + }), + } diff --git a/pms/templates/edit_booking_dates.html b/pms/templates/edit_booking_dates.html new file mode 100644 index 000000000..2b15e550a --- /dev/null +++ b/pms/templates/edit_booking_dates.html @@ -0,0 +1,53 @@ +{% extends "main.html"%} + +{% block content %} +
+
+

Editar Fechas - Reserva {{booking.code}}

+ +
+

Habitación: {{ booking.room.name }} ({{ booking.room.room_type.name }})

+

Cliente: {{ booking.customer.name }}

+

Precio por noche: €{{ booking.room.room_type.price }}

+
+ +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + +
+ + {{ form.checkin }} + {% if form.checkin.errors %} +
+ {{ form.checkin.errors.0 }} +
+ {% endif %} +
+ +
+ + {{ form.checkout }} + {% if form.checkout.errors %} +
+ {{ form.checkout.errors.0 }} +
+ {% endif %} +
+ + {% if form.non_field_errors %} +
+ {{ form.non_field_errors.0 }} +
+ {% endif %} + +
+ Cancelar + +
+
+
+
+{% endblock content%} \ No newline at end of file diff --git a/pms/templates/home.html b/pms/templates/home.html index 1e61b8024..9b4c7131d 100644 --- a/pms/templates/home.html +++ b/pms/templates/home.html @@ -68,7 +68,9 @@

Reservas Realizadas

Editar datos de contacto
- + {% if booking.state != "DEL" %} + Editar fechas + {% endif %}
diff --git a/pms/tests.py b/pms/tests.py index 7ce503c2d..a185026ce 100644 --- a/pms/tests.py +++ b/pms/tests.py @@ -1,3 +1,124 @@ from django.test import TestCase +from django.urls import reverse +from datetime import date, timedelta +from .models import Room, Room_type, Booking, Customer +from .forms import EditBookingDatesForm -# Create your tests here. + +class EditBookingDatesTests(TestCase): + """Comprehensive tests for booking dates edit functionality""" + + @classmethod + def setUpTestData(cls): + """Set up test data once for all tests""" + cls.room_type = Room_type.objects.create(name='Doble', price=30.0, max_guests=2) + cls.room_1 = Room.objects.create(name='Room 1.1', description='Test room', room_type=cls.room_type) + cls.customer = Customer.objects.create(name='Test User', email='test@emailtest.com', phone='123456789') + + def setUp(self): + """Create a booking for each test""" + self.today = date.today() + self.booking = Booking.objects.create( + code='TEST0001', + checkin=self.today + timedelta(days=5), + checkout=self.today + timedelta(days=10), + room=self.room_1, + guests=2, + customer=self.customer, + total=150.0, + state='NEW' + ) + + # Form tests + def test_form_has_correct_fields_and_labels(self): + """Test form structure and labels""" + form = EditBookingDatesForm() + self.assertEqual(list(form.fields.keys()), ['checkin', 'checkout']) + self.assertEqual(form.fields['checkin'].label, 'Fecha de entrada') + self.assertEqual(form.fields['checkout'].label, 'Fecha de salida') + + # View GET tests + def test_get_request_renders_form(self): + """Test GET request renders form with booking data""" + response = self.client.get(reverse('edit_booking_dates', kwargs={'pk': self.booking.id})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'edit_booking_dates.html') + self.assertIsInstance(response.context['form'], EditBookingDatesForm) + + # Successful edit tests + def test_successful_edit_updates_dates_and_recalculates_price(self): + """Test successful date change updates booking and recalculates total""" + new_checkin = self.today + timedelta(days=20) + new_checkout = self.today + timedelta(days=27) + + response = self.client.post(reverse('edit_booking_dates', kwargs={'pk': self.booking.id}), { + 'checkin': new_checkin.strftime('%Y-%m-%d'), + 'checkout': new_checkout.strftime('%Y-%m-%d') + }) + + self.assertRedirects(response, '/') + self.booking.refresh_from_db() + self.assertEqual(self.booking.checkin, new_checkin) + self.assertEqual(self.booking.checkout, new_checkout) + self.assertEqual(self.booking.total, 210.0) # 7 days * 30€ + + # Conflict detection tests + def test_edit_fails_when_room_occupied_in_new_dates(self): + """Test validation prevents overbooking""" + Booking.objects.create( + code='CONFLICT', + checkin=self.today + timedelta(days=20), + checkout=self.today + timedelta(days=25), + room=self.room_1, + guests=2, + customer=self.customer, + total=150.0, + state='NEW' + ) + + response = self.client.post(reverse('edit_booking_dates', kwargs={'pk': self.booking.id}), { + 'checkin': (self.today + timedelta(days=22)).strftime('%Y-%m-%d'), + 'checkout': (self.today + timedelta(days=27)).strftime('%Y-%m-%d') + }) + + self.assertEqual(response.status_code, 200) + self.assertFormError(response, 'form', None, 'No hay disponibilidad para las fechas seleccionadas') + + def test_edit_succeeds_when_conflict_is_canceled_booking(self): + """Test canceled bookings don't block availability""" + Booking.objects.create( + code='CANCELED', + checkin=self.today + timedelta(days=20), + checkout=self.today + timedelta(days=25), + room=self.room_1, + guests=2, + customer=self.customer, + total=150.0, + state='DEL' + ) + + response = self.client.post(reverse('edit_booking_dates', kwargs={'pk': self.booking.id}), { + 'checkin': (self.today + timedelta(days=22)).strftime('%Y-%m-%d'), + 'checkout': (self.today + timedelta(days=24)).strftime('%Y-%m-%d') + }) + + self.assertRedirects(response, '/') + + def test_booking_does_not_conflict_with_itself(self): + """Test booking can keep same dates without self-conflict""" + response = self.client.post(reverse('edit_booking_dates', kwargs={'pk': self.booking.id}), { + 'checkin': self.booking.checkin.strftime('%Y-%m-%d'), + 'checkout': self.booking.checkout.strftime('%Y-%m-%d') + }) + self.assertRedirects(response, '/') + + # Date validation tests + def test_validation_prevents_checkout_before_or_equal_checkin(self): + """Test checkout must be after checkin""" + response = self.client.post(reverse('edit_booking_dates', kwargs={'pk': self.booking.id}), { + 'checkin': (self.today + timedelta(days=20)).strftime('%Y-%m-%d'), + 'checkout': (self.today + timedelta(days=20)).strftime('%Y-%m-%d') + }) + + self.assertEqual(response.status_code, 200) + self.assertFormError(response, 'form', 'checkout', 'La fecha de salida debe ser posterior a la fecha de entrada') \ No newline at end of file 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..38a32df20 100644 --- a/pms/views.py +++ b/pms/views.py @@ -174,6 +174,71 @@ def post(self, request, pk): return redirect("/") +class EditBookingDatesView(View): + # Renders booking dates edition form + def get(self, request, pk): + booking = Booking.objects.get(id=pk) + form = EditBookingDatesForm(instance=booking) + context = { + 'form': form, + 'booking': booking + } + return render(request, "edit_booking_dates.html", context) + + # updates booking dates with availability validation + @method_decorator(ensure_csrf_cookie) + def post(self, request, pk): + booking = Booking.objects.get(id=pk) + form = EditBookingDatesForm(request.POST, instance=booking) + + if form.is_valid(): + new_checkin = form.cleaned_data['checkin'] + new_checkout = form.cleaned_data['checkout'] + + # Validate that checkout is after checkin + if new_checkout <= new_checkin: + form.add_error('checkout', 'La fecha de salida debe ser posterior a la fecha de entrada') + context = { + 'form': form, + 'booking': booking + } + return render(request, "edit_booking_dates.html", context) + + # Check room availability (exclude current booking from conflict check) + conflicting_bookings = Booking.objects.filter( + room=booking.room, + state='NEW' + ).exclude(id=booking.id).filter( + Q(checkin__lt=new_checkout) & Q(checkout__gt=new_checkin) + ) + + if conflicting_bookings.exists(): + form.add_error(None, 'No hay disponibilidad para las fechas seleccionadas') + context = { + 'form': form, + 'booking': booking + } + return render(request, "edit_booking_dates.html", context) + + # Recalculate total price + total_days = (new_checkout - new_checkin).days + new_total = total_days * booking.room.room_type.price + + # Update booking + booking.checkin = new_checkin + booking.checkout = new_checkout + booking.total = new_total + booking.save() + + return redirect("/") + + context = { + 'form': form, + 'booking': booking + } + return render(request, "edit_booking_dates.html", context) + + class DashboardView(View): def get(self, request): from datetime import date, time, datetime