Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions pms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}),
}
53 changes: 53 additions & 0 deletions pms/templates/edit_booking_dates.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{% extends "main.html"%}

{% block content %}
<div class="row d-flex justify-content-center">
<div class="col-12 col-md-6 col-lg-5 p-5 card shadow mt-5">
<h3 class="mb-4">Editar Fechas - Reserva {{booking.code}}</h3>

<div class="mb-3">
<p><strong>Habitación:</strong> {{ booking.room.name }} ({{ booking.room.room_type.name }})</p>
<p><strong>Cliente:</strong> {{ booking.customer.name }}</p>
<p><strong>Precio por noche:</strong> €{{ booking.room.room_type.price }}</p>
</div>

<form method="POST">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}

<div class="mb-3 mt-3">
<label for="{{ form.checkin.id_for_label }}" class="form-label">{{ form.checkin.label }}</label>
{{ form.checkin }}
{% if form.checkin.errors %}
<div class="text-danger mt-1">
{{ form.checkin.errors.0 }}
</div>
{% endif %}
</div>

<div class="mb-3">
<label for="{{ form.checkout.id_for_label }}" class="form-label">{{ form.checkout.label }}</label>
{{ form.checkout }}
{% if form.checkout.errors %}
<div class="text-danger mt-1">
{{ form.checkout.errors.0 }}
</div>
{% endif %}
</div>

{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors.0 }}
</div>
{% endif %}

<div class="d-flex justify-content-between mt-4">
<a href="{% url 'home' %}" class="btn btn-outline-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary">Guardar</button>
</div>
</form>
</div>
</div>
{% endblock content%}
4 changes: 3 additions & 1 deletion pms/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ <h3>Reservas Realizadas</h3>
<a href="{% url 'edit_booking' pk=booking.id%} " >Editar datos de contacto</a>
</div>
<div class="col">

{% if booking.state != "DEL" %}
<a href="{% url 'edit_booking_dates' pk=booking.id%} " >Editar fechas</a>
{% endif %}
</div>
<div class="col">

Expand Down
123 changes: 122 additions & 1 deletion pms/tests.py
Original file line number Diff line number Diff line change
@@ -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')
1 change: 1 addition & 0 deletions pms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
path("search/booking/", views.BookingSearchView.as_view(), name="booking_search"),
path("booking/<str:pk>/", views.BookingView.as_view(), name="booking"),
path("booking/<str:pk>/edit", views.EditBookingView.as_view(), name="edit_booking"),
path("booking/<str:pk>/edit-dates", views.EditBookingDatesView.as_view(), name="edit_booking_dates"),
path("booking/<str:pk>/delete", views.DeleteBookingView.as_view(), name="delete_booking"),
path("rooms/", views.RoomsView.as_view(), name="rooms"),
path("room/<str:pk>/", views.RoomDetailsView.as_view(), name="room_details"),
Expand Down
65 changes: 65 additions & 0 deletions pms/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down