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 }}
+
+
+
+
+
+{% 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