Skip to content
Closed
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
53 changes: 53 additions & 0 deletions docs/pr-3-edit-booking-dates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# PR 3 - Edición de Fechas de Reserva

## Objetivo de negocio
Habilitar la edición de fechas de una reserva ya creada, manteniendo reglas de disponibilidad del inventario y evitando conflictos operativos por solapamientos.

## Alcance implementado
- Se agregó un nuevo acceso en Home para **Editar fechas** por reserva.
- Se implementó una nueva pantalla dedicada para editar `checkin` y `checkout`.
- Se agregó validación de disponibilidad sobre la habitación actual antes de persistir cambios.
- Se incorporó recálculo automático del total de la reserva tras modificar fechas.
- Se incluyó validación de rango máximo permitido para reservas.

## Cambios funcionales y técnicos
1. **Navegación y routing**
- Nueva ruta para edición de fechas:
- `booking/<pk>/edit-dates`.
- Integración del nuevo acceso desde la lista de reservas.

2. **Lógica de aplicación**
- Se creó una vista específica para editar fechas, separada del flujo de edición de datos de contacto.
- Validaciones principales:
- `checkout > checkin`.
- límite de fechas permitido hasta `31/12/2026`.
- bloqueo por solape en la misma habitación excluyendo la reserva en edición.
- Mensaje de negocio mostrado cuando no hay disponibilidad:
- **“No hay disponibilidad para las fechas seleccionadas”**.

3. **Form y consistencia de datos**
- Se creó un formulario específico para edición de fechas.
- Se asegura render ISO en inputs de fecha (`YYYY-MM-DD`) para coherencia entre textbox y datepicker.

4. **Calidad**
- Se agregaron pruebas automatizadas para cubrir:
- edición exitosa con disponibilidad,
- render correcto de valores de fecha,
- rechazo por solape,
- rechazo por rango inválido.

## Archivos modificados
- `pms/forms.py`
- `pms/views.py`
- `pms/urls.py`
- `pms/templates/home.html`
- `pms/templates/edit_booking_dates.html`
- `pms/tests.py`

## Pruebas ejecutadas
- `python manage.py test pms.tests.EditBookingDatesTests`

## Resultado esperado para el producto
- Mayor flexibilidad operativa para atención al cliente post-reserva.
- Menor riesgo de sobreventa por validación consistente de disponibilidad.
- Mejora de confiabilidad del dato financiero al recalcular importe tras cambios de fechas.
45 changes: 44 additions & 1 deletion pms/forms.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import date, datetime
from django import forms
from django.forms import ModelForm

Expand Down Expand Up @@ -56,3 +56,46 @@ class Meta:
'total': forms.HiddenInput(),
'state': forms.HiddenInput(),
}


class BookingDatesEditForm(forms.Form):
checkin = forms.DateField(
label="Fecha de entrada",
input_formats=["%Y-%m-%d"],
widget=forms.DateInput(
format="%Y-%m-%d",
attrs={
"type": "date",
"max": date(2026, 12, 31).strftime("%Y-%m-%d"),
"class": "form-control",
}
),
)
checkout = forms.DateField(
label="Fecha de salida",
input_formats=["%Y-%m-%d"],
widget=forms.DateInput(
format="%Y-%m-%d",
attrs={
"type": "date",
"max": date(2026, 12, 31).strftime("%Y-%m-%d"),
"class": "form-control",
}
),
)

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 fecha de entrada")

max_allowed_date = date(2026, 12, 31)
if checkin > max_allowed_date or checkout > max_allowed_date:
raise forms.ValidationError("Solo se permiten reservas hasta el 31/12/2026")

return cleaned_data
38 changes: 38 additions & 0 deletions pms/templates/edit_booking_dates.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{% extends "main.html"%}

{% block content %}
<section class="rooms-page">
<div class="rooms-hero">
<h1 class="rooms-title">Editar fechas de reserva</h1>
<p class="rooms-subtitle">Reserva {{ booking.code }} - {{ booking.room.name }}</p>
</div>

<div class="card border-0 shadow-sm p-4">
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for error in form.non_field_errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}

<form action="" method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% for error in field.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}

<div class="d-flex gap-2">
<a class="btn btn-outline-primary" href="{% url 'home' %}">Volver</a>
<button class="btn btn-primary rooms-filter-btn" type="submit">Guardar</button>
</div>
</form>
</div>
</section>
{% endblock content %}
4 changes: 2 additions & 2 deletions pms/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ <h3>Reservas Realizadas</h3>
<a href="{% url 'edit_booking' pk=booking.id%} " >Editar datos de contacto</a>
</div>
<div class="col">

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

Expand All @@ -84,4 +84,4 @@ <h3>Reservas Realizadas</h3>
</div>
</div>

{% endblock content%}
{% endblock content%}
76 changes: 75 additions & 1 deletion pms/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,77 @@
from datetime import date, timedelta

from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse

from .models import Booking, Room, Room_type


@override_settings(STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage")
class EditBookingDatesTests(TestCase):
@classmethod
def setUpTestData(cls):
room_type = Room_type.objects.create(name="Simple", price=20, max_guests=1)
cls.room = Room.objects.create(room_type=room_type, name="Room 1.1", description="Desc")
cls.booking = Booking.objects.create(
state="NEW",
checkin=date.today() + timedelta(days=5),
checkout=date.today() + timedelta(days=7),
room=cls.room,
guests=1,
total=40,
code="EDIT0001",
)

def test_edit_dates_view_updates_booking_when_room_is_available(self):
response = self.client.post(
reverse("edit_booking_dates", kwargs={"pk": self.booking.id}),
data={
"checkin": date.today() + timedelta(days=10),
"checkout": date.today() + timedelta(days=12),
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/")

self.booking.refresh_from_db()
self.assertEqual(self.booking.checkin, date.today() + timedelta(days=10))
self.assertEqual(self.booking.checkout, date.today() + timedelta(days=12))
self.assertEqual(self.booking.total, 40)

def test_edit_dates_view_renders_iso_values_for_date_inputs(self):
response = self.client.get(reverse("edit_booking_dates", kwargs={"pk": self.booking.id}))
self.assertEqual(response.status_code, 200)
self.assertContains(response, f'value="{self.booking.checkin.strftime("%Y-%m-%d")}"')
self.assertContains(response, f'value="{self.booking.checkout.strftime("%Y-%m-%d")}"')

def test_edit_dates_view_displays_availability_error_when_dates_overlap(self):
Booking.objects.create(
state="NEW",
checkin=date.today() + timedelta(days=8),
checkout=date.today() + timedelta(days=11),
room=self.room,
guests=1,
total=60,
code="OVLP0001",
)
response = self.client.post(
reverse("edit_booking_dates", kwargs={"pk": self.booking.id}),
data={
"checkin": date.today() + timedelta(days=9),
"checkout": date.today() + timedelta(days=10),
},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No hay disponibilidad para las fechas seleccionadas")

# Create your tests here.
def test_edit_dates_view_displays_range_error_for_dates_after_2026_12_31(self):
response = self.client.post(
reverse("edit_booking_dates", kwargs={"pk": self.booking.id}),
data={
"checkin": date(2026, 12, 30),
"checkout": date(2027, 1, 2),
},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Solo se permiten reservas hasta el 31/12/2026")
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
45 changes: 45 additions & 0 deletions pms/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,51 @@ def post(self, request, pk):
return redirect("/")


class EditBookingDatesView(View):
def get(self, request, pk):
booking = Booking.objects.get(id=pk)
form = BookingDatesEditForm(
initial={
"checkin": booking.checkin,
"checkout": booking.checkout,
}
)
context = {
"booking": booking,
"form": form,
}
return render(request, "edit_booking_dates.html", context)

def post(self, request, pk):
booking = Booking.objects.get(id=pk)
form = BookingDatesEditForm(request.POST)
if form.is_valid():
checkin = form.cleaned_data["checkin"]
checkout = form.cleaned_data["checkout"]

has_overlap = (Booking.objects
.filter(room=booking.room, state="NEW")
.exclude(id=booking.id)
.filter(checkin__lte=checkout, checkout__gte=checkin)
.exists())

if has_overlap:
form.add_error(None, "No hay disponibilidad para las fechas seleccionadas")
else:
total_days = (checkout - checkin).days
booking.checkin = checkin
booking.checkout = checkout
booking.total = total_days * booking.room.room_type.price
booking.save(update_fields=["checkin", "checkout", "total"])
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
Expand Down