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
13 changes: 13 additions & 0 deletions pms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,16 @@ class Meta:
'state':forms.HiddenInput(),
}

class BookingDateForm(ModelForm):
class Meta:
model = Booking
fields = ['checkin', 'checkout']
labels = {
'checkin': 'Entrada',
'checkout': 'Salida'
}
widgets = {
'checkin': forms.DateInput(attrs={'type': 'date'}),
'checkout': forms.DateInput(attrs={'type': 'date'}),
}

19 changes: 19 additions & 0 deletions pms/services/booking_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from ..models import Booking


def is_room_available(room, checkin, checkout, exclude_booking_id=None):
"""
Valida si una habitación está disponible en las fechas especificadas.
bool: True si disponible, False si hay conflicto
"""
conflicts = Booking.objects.filter(
state=Booking.NEW,
room=room,
checkin__lte=checkout,
checkout__gte=checkin,
)

if exclude_booking_id:
conflicts = conflicts.exclude(id=exclude_booking_id)

return not conflicts.exists()
54 changes: 54 additions & 0 deletions pms/services/dashboard_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from datetime import date, datetime, time
from django.db.models import Sum
from django.utils.timezone import make_aware
from ..models import Booking, Room


def get_dashboard_data():
today = date.today()

today_min = make_aware(datetime.combine(today, time.min))
today_max = make_aware(datetime.combine(today, time.max))
today_range = (today_min, today_max)

# bookings created today
new_bookings = Booking.objects.filter(
created__range=today_range
).count()

# checkin guests
incoming = Booking.objects.filter(
checkin=today
).exclude(state="DEL").count()

# checkout guests
outcoming = Booking.objects.filter(
checkout=today
).exclude(state="DEL").count()

# invoiced today
invoiced = Booking.objects.filter(
created__range=today_range
).exclude(state="DEL").aggregate(total=Sum('total'))["total"] or 0

# total rooms
total_rooms = Room.objects.count()

confirmed_bookings = Booking.objects.filter(
state=Booking.NEW, # excluye canceladas aunque las fechas coincidan
checkin__lte=today,
checkout__gt=today,
).count()

occupancy_rate = (
(confirmed_bookings / total_rooms) * 100
if total_rooms > 0 else 0
)

return {
'new_bookings': new_bookings,
'incoming_guests': incoming,
'outcoming_guests': outcoming,
'invoiced': invoiced,
'occupancy_rate': occupancy_rate
}
17 changes: 17 additions & 0 deletions pms/statics/js/live_search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// pms/statics/js/live_search.js
document.addEventListener('DOMContentLoaded', function () {
const input = document.getElementById('live-search-input');
if (!input) return; // el componente no está en esta página, salir

const targetId = input.dataset.target;
const container = document.getElementById(targetId);
if (!container) return;

input.addEventListener('input', function () {
const query = input.value.toLowerCase().trim();
Array.from(container.children).forEach(function (card) {
const text = (card.dataset.search || '').toLowerCase();
card.style.display = text.includes(query) ? '' : 'none';
});
});
});
14 changes: 14 additions & 0 deletions pms/templates/components/live_search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{# components/live_search.html #}
{# Uso: {% include "components/live_search.html" with target_id="rooms-list" %} #}
<div class="row mb-3">
<div class="col-md-4">
<input
type="text"
id="live-search-input"
data-target="{{ target_id }}"
class="form-control"
placeholder="Buscar..."
autocomplete="off"
>
</div>
</div>
6 changes: 5 additions & 1 deletion pms/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ <h1 class="dashboard-value">{{dashboard.outcoming_guests}}</h1>

<div class="card text-white p-3 card-customization" style="background-color: #ff7f7f;">
<h5 class="small">Total facturado</h5>
<h1 class="dashboard-value">€ {% if dashboard.invoiced.total__sum == None %}0.00{% endif %} {{dashboard.invoiced.total__sum|floatformat:2}}</h1>
<h1 class="dashboard-value">€ {{ dashboard.invoiced|default:0|floatformat:2 }}</h1>
</div>
<div class="card text-white p-3 card-customization" style="background-color: #7b2d8b;">
<h5 class="small">% Ocupación</h5>
<h1 class="dashboard-value">{{dashboard.occupancy_rate|floatformat:1}}%</h1>
</div>
</div>
</div>
Expand Down
35 changes: 35 additions & 0 deletions pms/templates/edit_booking_date.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{% extends "main.html"%}

{% block content %}
<h1>Editar fechas</h1>
<div class="card card-body">
<form action="" method="post">
{% csrf_token %}
{% for field in date_form %}
<div class="row mb-3">
<div class="col-md-2">
{{field.label_tag}}
</div>
<div class="col-md-4">
{{field}}
</div>
</div>
{% endfor %}

{% if date_form.non_field_errors %}
<div class="alert alert-danger" role="alert">
{% for error in date_form.non_field_errors %}
{{error}}
{% endfor %}
</div>
{% endif %}

<div class="row">
<div class="col">
<a class="btn btn-outline-primary" href="{% url 'home' %}">Cancelar</a>
<button class="btn btn-primary" type="submit">Guardar fechas</button>
</div>
</div>
</form>
</div>
{% endblock content %}
7 changes: 3 additions & 4 deletions pms/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,18 @@ <h3>Reservas Realizadas</h3>
</div>
<div class="row">
<div class="col">

<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">

{% if booking.state != "DEL" %}
<a href="{% url 'delete_booking' pk=booking.id%} " >Cancelar reserva</a>
{% endif %}
</div>

</div>

</div>
Expand Down
1 change: 1 addition & 0 deletions pms/templates/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@

{% endblock %}
</div>
<script src="{% static 'js/live_search.js' %}"></script>
</body>
</html>
11 changes: 7 additions & 4 deletions pms/templates/rooms.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@

{% block content %}
<h1>Habitaciones del hotel</h1>
{% for room in rooms%}
<div class="row card mt-3 mb-3 hover-card bg-tr-250">

{% include "components/live_search.html" with target_id="rooms-list" %}

<div id="rooms-list">
{% for room in rooms %}
<div class="row card mt-3 mb-3 hover-card bg-tr-250" data-search="{{ room.name }} {{ room.room_type__name }}">
<div class="col p-3">
<div class="">
{{room.name}} ({{room.room_type__name}})
</div>
<div>
<a href="{% url 'room_details' pk=room.id%}">Ver detalles</a>
</div>

</div>

</div>
{% endfor %}
</div>
{% endblock content%}
Empty file added pms/tests/__init__.py
Empty file.
112 changes: 112 additions & 0 deletions pms/tests/test_booking_date_edit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from django.test import TestCase, override_settings
from django.urls import reverse
from datetime import date, timedelta
from ..models import Room, Room_type, Booking, Customer


@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
class EditBookingDateTest(TestCase):

def setUp(self):
room_type = Room_type.objects.create(name="Suite", price=100, max_guests=2)
self.customer = Customer.objects.create(name="Test", email="t@t.com", phone="123")
self.room = Room.objects.create(name="R1", description="", room_type=room_type)
self.today = date.today()

self.booking = Booking.objects.create(
room=self.room,
customer=self.customer,
state=Booking.NEW,
checkin=self.today + timedelta(days=5),
checkout=self.today + timedelta(days=7),
guests=2,
total=200,
code="BOOK0001",
)

def test_edit_booking_date_success(self):
new_checkin = self.today + timedelta(days=10)
new_checkout = self.today + timedelta(days=12)

response = self.client.post(
reverse("edit_booking_dates", args=[self.booking.id]),
{'checkin': new_checkin.isoformat(), 'checkout': new_checkout.isoformat()}
)

self.booking.refresh_from_db()
self.assertEqual(self.booking.checkin, new_checkin)
self.assertEqual(self.booking.checkout, new_checkout)
self.assertRedirects(response, '/')

def test_edit_booking_date_conflict_rejected(self):
Booking.objects.create(
room=self.room,
customer=self.customer,
state=Booking.NEW,
checkin=self.today + timedelta(days=15),
checkout=self.today + timedelta(days=18),
guests=2,
total=300,
code="BOOK0002",
)

new_checkin = self.today + timedelta(days=14)
new_checkout = self.today + timedelta(days=16)

response = self.client.post(
reverse("edit_booking_dates", args=[self.booking.id]),
{'checkin': new_checkin.isoformat(), 'checkout': new_checkout.isoformat()}
)

self.assertEqual(response.status_code, 200)
self.assertContains(response, "No hay disponibilidad")

def test_cancelled_booking_not_blocking(self):
Booking.objects.create(
room=self.room,
customer=self.customer,
state=Booking.DELETED,
checkin=self.today + timedelta(days=20),
checkout=self.today + timedelta(days=22),
guests=2,
total=200,
code="BOOK0003",
)

new_checkin = self.today + timedelta(days=20)
new_checkout = self.today + timedelta(days=22)

response = self.client.post(
reverse("edit_booking_dates", args=[self.booking.id]),
{'checkin': new_checkin.isoformat(), 'checkout': new_checkout.isoformat()}
)

self.assertRedirects(response, '/')

def test_concurrent_edit_protection(self):
other_booking = Booking.objects.create(
room=self.room,
customer=self.customer,
state=Booking.NEW,
checkin=self.today + timedelta(days=15),
checkout=self.today + timedelta(days=18),
guests=2,
total=300,
code="BOOK0004",
)

new_checkin = self.today + timedelta(days=14)
new_checkout = self.today + timedelta(days=16)

response = self.client.post(
reverse("edit_booking_dates", args=[self.booking.id]),
{'checkin': new_checkin.isoformat(), 'checkout': new_checkout.isoformat()}
)

self.assertEqual(response.status_code, 200)
self.assertContains(response, "No hay disponibilidad")

self.booking.refresh_from_db()
self.assertEqual(self.booking.checkin, self.today + timedelta(days=5))
self.assertEqual(self.booking.checkout, self.today + timedelta(days=7))

Loading