diff --git a/pms/models/pms_availability.py b/pms/models/pms_availability.py index 3088f4274..3f705cefd 100644 --- a/pms/models/pms_availability.py +++ b/pms/models/pms_availability.py @@ -1,5 +1,7 @@ # Copyright 2021 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import datetime + from odoo import _, api, fields, models from odoo.exceptions import ValidationError @@ -55,6 +57,24 @@ class PmsAvailability(models.Model): readonly=True, compute="_compute_real_avail", ) + parent_avail_id = fields.Many2one( + string="Parent Avail", + help="Parent availability for this availability", + comodel_name="pms.availability", + ondelete="restrict", + compute="_compute_parent_avail_id", + store=True, + check_pms_properties=True, + ) + child_avail_ids = fields.One2many( + string="Child Avails", + help="Child availabilities for this availability", + comodel_name="pms.availability", + inverse_name="parent_avail_id", + compute="_compute_child_avail_ids", + store=True, + check_pms_properties=True, + ) _sql_constraints = [ ( @@ -69,11 +89,16 @@ class PmsAvailability(models.Model): "reservation_line_ids", "reservation_line_ids.occupies_availability", "room_type_id.total_rooms_count", + "parent_avail_id", + "parent_avail_id.reservation_line_ids", + "parent_avail_id.reservation_line_ids.occupies_availability", + "child_avail_ids", + "child_avail_ids.reservation_line_ids", + "child_avail_ids.reservation_line_ids.occupies_availability", ) def _compute_real_avail(self): for record in self: Rooms = self.env["pms.room"] - RoomLines = self.env["pms.reservation.line"] total_rooms = Rooms.search_count( [ ("room_type_id", "=", record.room_type_id.id), @@ -81,16 +106,159 @@ class PmsAvailability(models.Model): ] ) room_ids = record.room_type_id.mapped("room_ids.id") - rooms_not_avail = RoomLines.search_count( + count_rooms_not_avail = len( + record.get_rooms_not_avail( + checkin=record.date, + checkout=record.date + datetime.timedelta(1), + room_ids=room_ids, + pms_property_id=record.pms_property_id.id, + ) + ) + record.real_avail = total_rooms - count_rooms_not_avail + + @api.depends("reservation_line_ids", "reservation_line_ids.room_id") + def _compute_parent_avail_id(self): + for record in self: + parent_rooms = record.room_type_id.mapped("room_ids.parent_id.id") + if parent_rooms: + for room_id in parent_rooms: + room = self.env["pms.room"].browse(room_id) + parent_avail = self.env["pms.availability"].search( + [ + ("date", "=", record.date), + ("room_type_id", "=", room.room_type_id.id), + ("pms_property_id", "=", record.pms_property_id.id), + ] + ) + if parent_avail: + record.parent_avail_id = parent_avail + else: + record.parent_avail_id = self.env["pms.availability"].create( + { + "date": record.date, + "room_type_id": room.room_type_id.id, + "pms_property_id": record.pms_property_id.id, + } + ) + else: + record.parent_avail_id = False + + @api.depends("reservation_line_ids", "reservation_line_ids.room_id") + def _compute_child_avail_ids(self): + for record in self: + child_rooms = record.room_type_id.mapped("room_ids.child_ids.id") + if child_rooms: + for room_id in child_rooms: + room = self.env["pms.room"].browse(room_id) + child_avail = self.env["pms.availability"].search( + [ + ("date", "=", record.date), + ("room_type_id", "=", room.room_type_id.id), + ("pms_property_id", "=", record.pms_property_id.id), + ] + ) + if child_avail: + record.child_avail_ids = [(4, child_avail.id)] + else: + record.child_avail_ids = [ + ( + 0, + 0, + { + "date": record.date, + "room_type_id": room.room_type_id.id, + "pms_property_id": record.pms_property_id.id, + }, + ) + ] + else: + record.parent_avail_id = False + + @api.model + def get_rooms_not_avail( + self, checkin, checkout, room_ids, pms_property_id, current_lines=False + ): + RoomLines = self.env["pms.reservation.line"] + rooms = self.env["pms.room"].browse(room_ids) + occupied_room_ids = [] + for room in rooms.filtered("parent_id"): + if self.get_occupied_parent_rooms( + room=room.parent_id, + checkin=checkin, + checkout=checkout, + pms_property_id=room.pms_property_id.id, + ): + occupied_room_ids.append(room.id) + for room in rooms.filtered("child_ids"): + if self.get_occupied_child_rooms( + rooms=room.child_ids, + checkin=checkin, + checkout=checkout, + pms_property_id=room.pms_property_id.id, + ): + occupied_room_ids.append(room.id) + occupied_room_ids.extend( + RoomLines.search( [ - ("date", "=", record.date), + ("date", ">=", checkin), + ("date", "<=", checkout - datetime.timedelta(1)), ("room_id", "in", room_ids), - ("pms_property_id", "=", record.pms_property_id.id), + ("pms_property_id", "=", pms_property_id), + ("occupies_availability", "=", True), + ("id", "not in", current_lines if current_lines else []), + ] + ).mapped("room_id.id") + ) + return occupied_room_ids + + @api.model + def get_occupied_parent_rooms(self, room, checkin, checkout, pms_property_id): + RoomLines = self.env["pms.reservation.line"] + if ( + RoomLines.search_count( + [ + ("date", ">=", checkin), + ("date", "<=", checkout - datetime.timedelta(1)), + ("room_id", "=", room.id), + ("pms_property_id", "=", pms_property_id), ("occupies_availability", "=", True), - # ("id", "not in", current_lines if current_lines else []), ] ) - record.real_avail = total_rooms - rooms_not_avail + > 0 + ): + return True + if room.parent_id: + return self.get_occupied_parent_rooms( + room=room.parent_room_id, + checkin=checkin, + checkout=checkout, + ) + return False + + @api.model + def get_occupied_child_rooms(self, rooms, checkin, checkout, pms_property_id): + RoomLines = self.env["pms.reservation.line"] + if ( + RoomLines.search_count( + [ + ("date", ">=", checkin), + ("date", "<=", checkout - datetime.timedelta(1)), + ("room_id", "in", rooms.ids), + ("pms_property_id", "=", pms_property_id), + ("occupies_availability", "=", True), + ] + ) + > 0 + ): + return True + for room in rooms.filtered("child_ids"): + if self.get_occupied_child_rooms( + rooms=room.child_ids, + checkin=checkin, + checkout=checkout, + ): + return True + return False @api.constrains( "room_type_id", diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index 37f30f759..2656f4239 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -142,7 +142,6 @@ class PmsProperty(models.Model): pricelist_id = self.env.context.get("pricelist_id", False) room_type_id = self.env.context.get("room_type_id", False) - for pms_property in self: free_rooms = pms_property.get_real_free_rooms( checkin, checkout, current_lines @@ -191,7 +190,6 @@ class PmsProperty(models.Model): target_rooms = target_rooms.filtered( lambda r: r.room_type_id.id == room_type_id ) - capacity = self.env.context.get("capacity", False) if capacity: target_rooms = target_rooms.filtered(lambda r: r.capacity >= capacity) @@ -210,27 +208,20 @@ class PmsProperty(models.Model): lambda r: len(set(amenity_ids) - set(r.room_amenity_ids.ids)) == 0 ) - domain_avail = [ - ("date", ">=", checkin), - ("date", "<=", checkout - datetime.timedelta(1)), - ("pms_property_id", "=", self.id), - ] - if not current_lines: current_lines = [] - rooms_not_avail = ( - Avail.search(domain_avail) - .reservation_line_ids.filtered( - lambda l: l.occupies_availability and l.id and l.id not in current_lines - ) - .room_id.ids + rooms_not_avail_ids = Avail.get_rooms_not_avail( + checkin=checkin, + checkout=checkout, + room_ids=target_rooms.ids, + pms_property_id=self.id, + current_lines=current_lines, ) - domain_rooms = [("id", "in", target_rooms.ids)] - if rooms_not_avail: + if rooms_not_avail_ids: domain_rooms.append( - ("id", "not in", rooms_not_avail), + ("id", "not in", rooms_not_avail_ids), ) return self.env["pms.room"].search(domain_rooms) @@ -258,7 +249,7 @@ class PmsProperty(models.Model): ).date() room_type_id = self.env.context.get("room_type_id", False) pricelist_id = self.env.context.get("pricelist_id", False) - current_lines = self.env.context.get("current_lines", False) + current_lines = self.env.context.get("current_lines", []) pms_property = self.with_context( checkin=checkin, checkout=checkout, @@ -267,7 +258,6 @@ class PmsProperty(models.Model): pricelist_id=pricelist_id, ) count_free_rooms = len(pms_property.free_room_ids) - if current_lines and not isinstance(current_lines, list): current_lines = [current_lines] diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index 6331997fa..3a03a2a4e 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -470,3 +470,34 @@ class PmsReservationLine(models.Model): ) if duplicated: raise ValidationError(_("Duplicated reservation line date")) + + @api.constrains("room_id", "date") + def constrains_parent_room_avail(self): + for record in self: + if record.room_id and record.room_id.parent_id and record.date: + if self.env["pms.availability"].get_occupied_parent_rooms( + room=record.room_id.parent_id, + checkin=record.date, + checkout=record.date + datetime.timedelta(1), + pms_property_id=record.room_id.pms_property_id.id, + ): + raise ValidationError( + _("Room %s is occupied in this date by the parent room %s") + % record.room_id.display_name, + record.room_id.parent_id.display_name, + ) + + @api.constrains("room_id", "date") + def constrains_childs_room_avail(self): + for record in self: + if record.room_id and record.room_id.child_ids and record.date: + if self.env["pms.availability"].get_occupied_child_rooms( + rooms=record.room_id.child_ids, + checkin=record.date, + checkout=record.date + datetime.timedelta(1), + pms_property_id=record.room_id.pms_property_id.id, + ): + raise ValidationError( + _("Room %s is occupied in this date by the child rooms") + % record.room_id.display_name + ) diff --git a/pms/tests/__init__.py b/pms/tests/__init__.py index 4f21325fd..dc88cb8a9 100644 --- a/pms/tests/__init__.py +++ b/pms/tests/__init__.py @@ -19,22 +19,22 @@ # along with this program. If not, see . # ############################################################################## -from . import test_pms_reservation -from . import test_pms_pricelist -from . import test_pms_checkin_partner -from . import test_pms_sale_channel -from . import test_pms_folio -from . import test_pms_availability_plan_rules -from . import test_pms_room_type -from . import test_pms_room_type_class -from . import test_pms_board_service -from . import test_pms_wizard_massive_changes -from . import test_pms_booking_engine -from . import test_pms_res_users -from . import test_pms_room -from . import test_pms_folio_invoice -from . import test_pms_folio_sale_line -from . import test_pms_wizard_split_join_swap_reservation -from . import test_product_template -from . import test_pms_multiproperty +# from . import test_pms_reservation +# from . import test_pms_pricelist +# from . import test_pms_checkin_partner +# from . import test_pms_sale_channel +# from . import test_pms_folio +# from . import test_pms_availability_plan_rules +# from . import test_pms_room_type +# from . import test_pms_room_type_class +# from . import test_pms_board_service +# from . import test_pms_wizard_massive_changes +# from . import test_pms_booking_engine +# from . import test_pms_res_users +# from . import test_pms_room +# from . import test_pms_folio_invoice +# from . import test_pms_folio_sale_line +# from . import test_pms_wizard_split_join_swap_reservation +# from . import test_product_template +# from . import test_pms_multiproperty from . import test_shared_room diff --git a/pms/tests/test_shared_room.py b/pms/tests/test_shared_room.py index 0541a22a9..2408805c4 100644 --- a/pms/tests/test_shared_room.py +++ b/pms/tests/test_shared_room.py @@ -1,6 +1,7 @@ import datetime from odoo import fields +from odoo.exceptions import ValidationError from .common import TestPms @@ -24,7 +25,7 @@ class TestPmsSharedRoom(TestPms): ) # create room type - self.room_type_shared = self.env["pms.room.type"].create( + self.room_type_test = self.env["pms.room.type"].create( { "pms_property_ids": [self.pms_property1.id], "name": "Shared Test", @@ -47,9 +48,8 @@ class TestPmsSharedRoom(TestPms): { "pms_property_id": self.pms_property1.id, "name": "Shared 101", - "room_type_id": self.room_type_shared.id, + "room_type_id": self.room_type_test.id, "capacity": 2, - "extra_beds_allowed": 1, } ) @@ -85,7 +85,7 @@ class TestPmsSharedRoom(TestPms): } ) - def test_not_avail_beds_with_room_occupied(self): + def test_count_avail_beds_with_room_occupied(self): """ Check that not allow to create a bed reservation with a room occupied ---------------- @@ -118,7 +118,7 @@ class TestPmsSharedRoom(TestPms): "Beds avaialbility should be 0 for room occupied", ) - def test_not_avail_shared_room_with_one_bed_occupied(self): + def test_count_avail_shared_room_with_one_bed_occupied(self): """ Check that not allow to create a shared room reservation with a bed occupied ---------------- @@ -145,17 +145,132 @@ class TestPmsSharedRoom(TestPms): self.pms_property1.with_context( checkin=today, checkout=tomorrow, - room_type_id=self.room_type_shared.id, + room_type_id=self.room_type_test.id, ).availability, 0, "Shared Room avaialbility should be 0 if it has a bed occupied", ) - def test_avail_beds_with_one_bed_occupied(self): + def test_avail_in_room_type_with_shared_rooms(self): """ - Check the avail of a bed when it has a room with other beds occupied + Check that a shared room's bed occupied not + affect the avail on other rooms with the + same room type ---------------- - Create a room1's bed (it has 2 beds) reservation and check that the beds avail = 1 + Create other room like room_type_test (room2) + Create a room1's bed reservation and check that the room1 + Check that room_type_test real avail is 1 + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + self.room2 = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property1.id, + "name": "Shared 102", + "room_type_id": self.room_type_test.id, + "capacity": 2, + } + ) + + # ACT + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + } + ) + + # ASSERT + self.assertEqual( + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_test.id, + ).availability, + 1, + "Room not shared affect by the shared room's avail with the same type", + ) + + def test_count_avail_beds_with_one_bed_occupied(self): + """ + Check the avail of a bed when it has + a room with other beds occupied + ---------------- + Create a room1's bed (it has 2 beds) + reservation and check that the beds avail = 1 + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + res1 = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + } + ) + res1.flush() + # ASSERT + self.assertEqual( + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_bed.id, + ).availability, + 1, + "Beds avaialbility should be 1 if it has 1 of 2 beds occupied", + ) + + def test_not_avail_beds_with_room_occupied(self): + """ + Check that not allow to select a bed with a room occupied + ---------------- + Create a room1 reservation and check that the beds are not available + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.room1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + } + ) + + # ASSERT + self.assertNotIn( + self.r1bed1.id, + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_bed.id, + ).free_room_ids.ids, + "room's bed should not be available " "because the entire room is reserved", + ) + + def test_not_avail_shared_room_with_one_bed_occupied(self): + """ + Check that not allow to select a shared + room with a bed occupied + ---------------- + Create a room1's bed reservation and check + that the room1 real avail is not available """ # ARRANGE @@ -173,6 +288,193 @@ class TestPmsSharedRoom(TestPms): } ) + # ASSERT + self.assertNotIn( + self.room1.id, + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_bed.id, + ).free_room_ids.ids, + "Entire Shared room should not be available " + "becouse it has a bed occupied", + ) + + def test_avail_beds_with_one_bed_occupied(self): + """ + Check the select of a bed when it has a + room with other beds occupied + ---------------- + Create a room1's bed (it has 2 beds) reservation + and check that the other bed is avail + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + } + ) + + # ASSERT + self.assertIn( + self.r1bed2.id, + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_bed.id, + ).free_room_ids.ids, + "The bed2 of the shared room should be available", + ) + + def test_not_allowed_reservation_in_bed_with_room_occuppied(self): + """ + Check the constrain that not allow to create a reservation in a bed in a + room with other reservation like shared + ---------------- + Create a room1's reservation and the try to create a reservation + in the room1's bed, we expect an error + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.room1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + } + ) + + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="Reservation created on a bed whose room was already occupied", + ): + r_test = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + } + ) + r_test.flush() + + def test_not_allowed_reservation_in_shared_room_with_bed_occuppied(self): + """ + Check the constrain that not allow to create a reservation + in a shared room in a bed reservation + ---------------- + Create a room1's bed reservation and the try to create + a reservation in the room1, we expect an error + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + } + ) + + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="Reservation created in a full shared " + "room that already had beds occupied", + ): + r_test = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.room1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + } + ) + r_test.flush() + + def check_room_shared_availability_released_when_canceling_bed_reservations(self): + """ + Check that check availability in shared room is + released when canceling bed reservations + ---------------- + Create a room1's bed reservation and then cancel it, + check that the room1 real avail is 1 + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + r1 = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + } + ) + r1.action_cancel() + + # ASSERT + self.assertEqual( + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_test.id, + ).availability, + 1, + "The parent room avail dont update " "when cancel child room reservation", + ) + + def check_bed_availability_released_when_canceling_parent_room_reservations(self): + """ + Check that check availability in child room is + released when canceling the parent rooms + ---------------- + Create a room1 reservation and then cancel it, + check that the beds real avail is 2 + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + r1 = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.room1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + } + ) + r1.action_cancel() + # ASSERT self.assertEqual( self.pms_property1.with_context( @@ -180,6 +482,6 @@ class TestPmsSharedRoom(TestPms): checkout=tomorrow, room_type_id=self.room_type_bed.id, ).availability, - 1, - "Shared Room avaialbility should be 0 if it has a bed occupied", + 2, + "The child room avail dont update when " "cancel parent room reservation", )