From 6080db54bef51e58cb76eda5619498a70c5da3b1 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 30 Nov 2020 17:27:59 +0100 Subject: [PATCH] [IMP] pms: availability plans --- pms/models/pms_reservation.py | 17 +- pms/models/pms_reservation_line.py | 50 +- pms/models/pms_room_type_restriction.py | 170 +++++- pms/models/pms_room_type_restriction_item.py | 91 ++- pms/models/product_pricelist.py | 6 + pms/tests/__init__.py | 1 + pms/tests/test_pms_room_type_restriction.py | 574 ++++++++++++++++++ .../pms_room_type_restriction_item_views.xml | 9 +- pms/views/pms_room_type_restriction_views.xml | 31 +- 9 files changed, 898 insertions(+), 51 deletions(-) create mode 100644 pms/tests/test_pms_room_type_restriction.py diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index e12858a97..ed7da2781 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -507,7 +507,11 @@ class PmsReservation(models.Model): ) @api.depends( - "reservation_line_ids.date", "overbooking", "state", "preferred_room_id" + "reservation_line_ids.date", + "overbooking", + "state", + "preferred_room_id", + "pricelist_id", ) def _compute_allowed_room_ids(self): for reservation in self: @@ -517,13 +521,12 @@ class PmsReservation(models.Model): [("active", "=", True)] ) return - rooms_available = self.env[ - "pms.room.type.availability" - ].rooms_available( + rooms_available = self.env["pms.room.type.restriction"].rooms_available( checkin=reservation.checkin, checkout=reservation.checkout, room_type_id=False, # Allow chosen any available room current_lines=reservation.reservation_line_ids.ids, + pricelist=reservation.pricelist_id.id, ) reservation.allowed_room_ids = rooms_available @@ -1098,10 +1101,11 @@ class PmsReservation(models.Model): } def open_reservation_wizard(self): - rooms_available = self.env["pms.room.type.availability"].rooms_available( + rooms_available = self.env["pms.room.type.restriction"].rooms_available( checkin=self.checkin, checkout=self.checkout, current_lines=self.reservation_line_ids.ids, + pricelist=self.pricelist_id.id, ) # REVIEW: check capacity room return { @@ -1180,10 +1184,11 @@ class PmsReservation(models.Model): def _autoassign(self): self.ensure_one() room_chosen = False - rooms_available = self.env["pms.room.type.availability"].rooms_available( + rooms_available = self.env["pms.room.type.restriction"].rooms_available( checkin=self.checkin, checkout=self.checkout, room_type_id=self.room_type_id.id or False, + pricelist=self.pricelist_id.id, ) if rooms_available: room_chosen = rooms_available[0] diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index 59cb12a70..1f0f77c51 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -80,6 +80,12 @@ class PmsReservationLine(models.Model): store=True, help="This record is taken into account to calculate availability", ) + impacts_quota = fields.Integer( + string="Impacts quota", + compute="_compute_impact_quota", + store=True, + readonly=False, + ) _sql_constraints = [ ( @@ -102,17 +108,15 @@ class PmsReservationLine(models.Model): # select room_id regardless room_type_id selected on reservation free_room_select = True if reservation.preferred_room_id else False # we get the rooms available for the entire stay - rooms_available = self.env[ - "pms.room.type.availability" - ].rooms_available( - checkin=reservation.checkin, - checkout=reservation.checkout, + rooms_available = self.env["pms.room.type.restriction"].rooms_available( + checkin=line.reservation_id.checkin, + checkout=line.reservation_id.checkout, room_type_id=reservation.room_type_id.id if not free_room_select else False, current_lines=line._origin.reservation_id.reservation_line_ids.ids, + pricelist=line.reservation_id.pricelist_id.id, ) - # if there is availability for the entire stay if rooms_available: @@ -134,9 +138,20 @@ class PmsReservationLine(models.Model): # available for the entire stay else: line.room_id = rooms_available[0] + # check that the reservation cannot be allocated even by dividing it + elif not self.env["pms.room.type.restriction"].splitted_availability( + checkin=line.reservation_id.checkin, + checkout=line.reservation_id.checkout, + room_type_id=line.reservation_id.room_type_id.id, + current_lines=line._origin.reservation_id.reservation_line_ids.ids, + pricelist=line.reservation_id.pricelist_id, + ): + raise ValidationError( + _("%s: No room type available") + % (line.reservation_id.room_type_id.name) + ) - # if there is no availability for the entire stay without - # changing rooms (we assume a split reservation) + # the reservation can be allocated into several rooms else: rooms_ranking = dict() @@ -173,12 +188,8 @@ class PmsReservationLine(models.Model): if room.id not in rooms_ranking else rooms_ranking[room.id] + 1 ) - if len(rooms_ranking) == 0: - raise ValidationError( - _("%s: No room type available") - % (reservation.room_type_id.name) - ) - else: + + if len(rooms_ranking) > 0: # we get the best score in the ranking best = max(rooms_ranking.values()) @@ -218,6 +229,17 @@ class PmsReservationLine(models.Model): # no matter what it is line.room_id = list(bests.keys())[0] + @api.depends("reservation_id.room_type_id", "reservation_id.pricelist_id") + def _compute_impact_quota(self): + for line in self: + reservation = line.reservation_id + line.impacts_quota = self.env["pms.room.type.restriction"].update_quota( + pricelist_id=reservation.pricelist_id, + room_type_id=reservation.room_type_id, + date=line.date, + line=line, + ) + @api.depends( "reservation_id", "reservation_id.pricelist_id", diff --git a/pms/models/pms_room_type_restriction.py b/pms/models/pms_room_type_restriction.py index ea27914af..5f3e45bfd 100644 --- a/pms/models/pms_room_type_restriction.py +++ b/pms/models/pms_room_type_restriction.py @@ -1,5 +1,7 @@ # Copyright 2017 Alexandre Díaz # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import datetime + from odoo import api, fields, models @@ -18,20 +20,176 @@ class PmsRoomTypeRestriction(models.Model): # Fields declaration name = fields.Char("Restriction Plan Name", required=True) pms_property_id = fields.Many2one( - "pms.property", - "Property", + comodel_name="pms.property", + string="Property", ondelete="restrict", - default=_get_default_pms_property, ) + + pms_pricelist_ids = fields.One2many( + comodel_name="product.pricelist", + inverse_name="restriction_id", + string="Pricelists", + required=False, + ondelete="restrict", + ) + item_ids = fields.One2many( - "pms.room.type.restriction.item", - "restriction_id", + comodel_name="pms.room.type.restriction.item", + inverse_name="restriction_id", string="Restriction Items", copy=True, ) + active = fields.Boolean( - "Active", + string="Active", default=True, help="If unchecked, it will allow you to hide the " "restriction plan without removing it.", ) + + # Business Methods + @classmethod + def any_restriction_applies(cls, checkin, checkout, item): + reservation_len = (checkout - checkin).days + return any( + [ + (0 < item.max_stay < reservation_len), + (0 < item.min_stay > reservation_len), + (0 < item.max_stay_arrival < reservation_len and checkin == item.date), + (0 < item.min_stay_arrival > reservation_len and checkin == item.date), + item.closed, + (item.closed_arrival and checkin == item.date), + (item.closed_departure and checkout == item.date), + (item.quota == 0 or item.max_avail == 0), + ] + ) + + @api.model + def rooms_available( + self, + checkin, + checkout, + room_type_id=False, + current_lines=False, + pricelist=False, + ): + if current_lines and not isinstance(current_lines, list): + current_lines = [current_lines] + + rooms_not_avail = ( + self.env["pms.reservation.line"] + .search( + [ + ("date", ">=", checkin), + ("date", "<=", checkout - datetime.timedelta(1)), + ("occupies_availability", "=", True), + ("id", "not in", current_lines if current_lines else []), + ] + ) + .mapped("room_id.id") + ) + + domain_rooms = [ + ("id", "not in", rooms_not_avail if len(rooms_not_avail) > 0 else []) + ] + domain_restrictions = [ + ("date", ">=", checkin), + ("date", "<=", checkout), + ] + + if room_type_id: + domain_rooms.append(("room_type_id", "=", room_type_id)) + domain_restrictions.append(("room_type_id", "=", room_type_id)) + + free_rooms = self.env["pms.room"].search(domain_rooms) + + if pricelist: + domain_restrictions.append( + ("restriction_id.pms_pricelist_ids", "=", pricelist) + ) + restriction_items = self.env["pms.room.type.restriction.item"].search( + domain_restrictions + ) + + if len(restriction_items) > 0: + room_types_to_remove = [] + for item in restriction_items: + if self.any_restriction_applies(checkin, checkout, item): + room_types_to_remove.append(item.room_type_id.id) + free_rooms = free_rooms.filtered( + lambda x: x.room_type_id.id not in room_types_to_remove + ) + + return free_rooms.sorted(key=lambda r: r.sequence) + + @api.model + def splitted_availability( + self, + checkin, + checkout, + room_type_id=False, + current_lines=False, + pricelist=False, + ): + for date_iterator in [ + checkin + datetime.timedelta(days=x) + for x in range(0, (checkout - checkin).days) + ]: + rooms_avail = self.rooms_available( + checkin=date_iterator, + checkout=date_iterator + datetime.timedelta(1), + room_type_id=room_type_id, + current_lines=current_lines, + pricelist=pricelist.id, + ) + if len(rooms_avail) < 1: + return False + return True + + @api.model + def update_quota(self, pricelist_id, room_type_id, date, line): + if pricelist_id and room_type_id and date: + restriction = self.env["pms.room.type.restriction.item"].search( + [ + ("restriction_id.pms_pricelist_ids", "=", pricelist_id.id), + ("room_type_id", "=", room_type_id.id), + ("date", "=", date), + ] + ) + # applies a restriction + if restriction: + restriction.ensure_one() + if restriction and restriction.quota != -1 and restriction.quota > 0: + + # the line has no restriction item applied before + if not line.impacts_quota: + restriction.quota -= 1 + return restriction.id + + # the line has a restriction item applied before + elif line.impacts_quota != restriction.id: + + # decrement quota on current restriction_item + restriction.quota -= 1 + + # check old restricition item + old_restriction = self.env[ + "pms.room.type.restriction.item" + ].search([("id", "=", line.impacts_quota)]) + + # restore quota in old restriction item + if old_restriction: + old_restriction.quota += 1 + + return restriction.id + + # in any case, check old restricition item + if line.impacts_quota: + old_restriction = self.env["pms.room.type.restriction.item"].search( + [("id", "=", line.impacts_quota)] + ) + # and restore quota in old restriction item + if old_restriction: + old_restriction.quota += 1 + + return False diff --git a/pms/models/pms_room_type_restriction_item.py b/pms/models/pms_room_type_restriction_item.py index e1cacc06a..35b4cb34c 100644 --- a/pms/models/pms_room_type_restriction_item.py +++ b/pms/models/pms_room_type_restriction_item.py @@ -9,21 +9,64 @@ class PmsRoomTypeRestrictionItem(models.Model): _description = "Reservation restriction by day" # Field Declarations + restriction_id = fields.Many2one( - "pms.room.type.restriction", "Restriction Plan", ondelete="cascade", index=True + comodel_name="pms.room.type.restriction", + string="Restriction Plan", + ondelete="cascade", + index=True, ) room_type_id = fields.Many2one( - "pms.room.type", "Room Type", required=True, ondelete="cascade" + comodel_name="pms.room.type", + string="Room Type", + required=True, + ondelete="cascade", ) - date = fields.Date("Date") + date = fields.Date(string="Date") - min_stay = fields.Integer("Min. Stay") - min_stay_arrival = fields.Integer("Min. Stay Arrival") - max_stay = fields.Integer("Max. Stay") - max_stay_arrival = fields.Integer("Max. Stay Arrival") - closed = fields.Boolean("Closed") - closed_departure = fields.Boolean("Closed Departure") - closed_arrival = fields.Boolean("Closed Arrival") + min_stay = fields.Integer( + string="Min. Stay", + default=0, + ) + min_stay_arrival = fields.Integer( + string="Min. Stay Arrival", + default=0, + ) + max_stay = fields.Integer( + string="Max. Stay", + default=0, + ) + max_stay_arrival = fields.Integer( + string="Max. Stay Arrival", + default=0, + ) + closed = fields.Boolean( + string="Closed", + default=False, + ) + closed_departure = fields.Boolean( + string="Closed Departure", + default=False, + ) + closed_arrival = fields.Boolean( + string="Closed Arrival", + default=False, + ) + quota = fields.Integer( + string="Quota", + store=True, + readonly=False, + compute="_compute_quota", + help="Generic Quota assigned.", + ) + + max_avail = fields.Integer( + string="Max. Availability", + store=True, + readonly=False, + compute="_compute_max_avail", + help="Maximum simultaneous availability on own Booking Engine.", + ) _sql_constraints = [ ( @@ -34,10 +77,20 @@ class PmsRoomTypeRestrictionItem(models.Model): ) ] - # Constraints and onchanges + @api.depends("room_type_id") + def _compute_quota(self): + for record in self: + if not record.quota: + record.quota = record.room_type_id.default_quota + + @api.depends("room_type_id") + def _compute_max_avail(self): + for record in self: + if not record.max_avail: + record.max_avail = record.room_type_id.default_max_avail @api.constrains("min_stay", "min_stay_arrival", "max_stay", "max_stay_arrival") - def _check_min_stay(self): + def _check_min_max_stay(self): for record in self: if record.min_stay < 0: raise ValidationError(_("Min. Stay can't be less than zero")) @@ -47,3 +100,17 @@ class PmsRoomTypeRestrictionItem(models.Model): raise ValidationError(_("Max. Stay can't be less than zero")) elif record.max_stay_arrival < 0: raise ValidationError(_("Max. Stay Arrival can't be less than zero")) + elif ( + record.min_stay != 0 + and record.max_stay != 0 + and record.min_stay > record.max_stay + ): + raise ValidationError(_("Max. Stay can't be less than Min. Stay")) + elif ( + record.min_stay_arrival != 0 + and record.max_stay_arrival != 0 + and record.min_stay_arrival > record.max_stay_arrival + ): + raise ValidationError( + _("Max. Stay Arrival can't be less than Min. Stay Arrival") + ) diff --git a/pms/models/product_pricelist.py b/pms/models/product_pricelist.py index da96b3fd8..ab19b1ae3 100644 --- a/pms/models/product_pricelist.py +++ b/pms/models/product_pricelist.py @@ -22,6 +22,12 @@ class ProductPricelist(models.Model): [("daily", "Daily Plan")], string="Pricelist Type", default="daily" ) + restriction_id = fields.Many2one( + comodel_name="pms.room.type.restriction", + string="restriction", + ondelete="restrict", + ) + # Constraints and onchanges # @api.constrains("pricelist_type", "pms_property_ids") # def _check_pricelist_type_property_ids(self): diff --git a/pms/tests/__init__.py b/pms/tests/__init__.py index 05b547df4..63b77ba65 100644 --- a/pms/tests/__init__.py +++ b/pms/tests/__init__.py @@ -25,3 +25,4 @@ from . import test_pms_pricelist_priority from . import test_pms_checkin_partner from . import test_pms_sale_channel from . import test_pms_folio +from . import test_pms_room_type_restriction diff --git a/pms/tests/test_pms_room_type_restriction.py b/pms/tests/test_pms_room_type_restriction.py new file mode 100644 index 000000000..6203c1e44 --- /dev/null +++ b/pms/tests/test_pms_room_type_restriction.py @@ -0,0 +1,574 @@ +import datetime + +from _pytest.skipping import Skip +from freezegun import freeze_time + +from odoo import fields +from odoo.exceptions import ValidationError + +from .common import TestHotel + + +@freeze_time("1980-01-01") +class TestPmsRoomTypeRestriction(TestHotel): + def create_common_scenario(self): + # product.pricelist + self.test_pricelist1 = self.env["product.pricelist"].create( + { + "name": "test pricelist 1", + } + ) + # pms.room.type.restriction + self.test_room_type_restriction1 = self.env["pms.room.type.restriction"].create( + { + "name": "Restriction plan for TEST", + "pms_pricelist_ids": [(6, 0, [self.test_pricelist1.id])], + } + ) + # pms.property + self.test_property = self.env["pms.property"].create( + { + "name": "MY PMS TEST", + "company_id": self.env.ref("base.main_company").id, + "default_pricelist_id": self.test_pricelist1.id, + "default_restriction_id": self.test_room_type_restriction1.id, + } + ) + # pms.room.type.class + self.test_room_type_class = self.env["pms.room.type.class"].create( + {"name": "Room"} + ) + + # pms.room.type + self.test_room_type_single = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.test_property.id], + "name": "Single Test", + "code_type": "SNG_Test", + "class_id": self.test_room_type_class.id, + } + ) + # pms.room.type + self.test_room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.test_property.id], + "name": "Double Test", + "code_type": "DBL_Test", + "class_id": self.test_room_type_class.id, + } + ) + # pms.room + self.test_room1_double = self.env["pms.room"].create( + { + "pms_property_id": self.test_property.id, + "name": "Double 201 test", + "room_type_id": self.test_room_type_double.id, + "capacity": 2, + } + ) + # pms.room + self.test_room2_double = self.env["pms.room"].create( + { + "pms_property_id": self.test_property.id, + "name": "Double 202 test", + "room_type_id": self.test_room_type_double.id, + "capacity": 2, + } + ) + # pms.room + # self.test_room3_double = self.env["pms.room"].create( + # { + # "pms_property_id": self.test_property.id, + # "name": "Double 203 test", + # "room_type_id": self.test_room_type_double.id, + # "capacity": 2, + # } + # ) + # # pms.room + # self.test_room4_double = self.env["pms.room"].create( + # { + # "pms_property_id": self.test_property.id, + # "name": "Double 204 test", + # "room_type_id": self.test_room_type_double.id, + # "capacity": 2, + # } + # ) + # pms.room + self.test_room1_single = self.env["pms.room"].create( + { + "pms_property_id": self.test_property.id, + "name": "Single 101 test", + "room_type_id": self.test_room_type_single.id, + "capacity": 1, + } + ) + # pms.room + self.test_room2_single = self.env["pms.room"].create( + { + "pms_property_id": self.test_property.id, + "name": "Single 102 test", + "room_type_id": self.test_room_type_single.id, + "capacity": 1, + } + ) + + @Skip + def test_availability_rooms_all(self): + # TEST CASE + # get availability withouth restrictions + + # ARRANGE + self.create_common_scenario() + + checkin = fields.date.today() + checkout = (fields.datetime.today() + datetime.timedelta(days=4)).date() + test_rooms_double_rooms = self.env["pms.room"].search( + [("pms_property_id", "=", self.test_property.id)] + ) + + # ACT + result = self.env["pms.room.type.restriction"].rooms_available( + checkin=checkin, + checkout=checkout, + ) + # ASSERT + obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms) + self.assertTrue( + obtained, + "Availability should contain the test rooms" + "because there's no restriction for them.", + ) + + @Skip + def test_availability_rooms_all_lines(self): + # TEST CASE + # get availability withouth restrictions + # given reservation lines to not consider + + # ARRANGE + self.create_common_scenario() + checkin = fields.date.today() + checkout = (fields.datetime.today() + datetime.timedelta(days=4)).date() + test_rooms_double_rooms = self.env["pms.room"].search( + [("pms_property_id", "=", self.test_property.id)] + ) + test_reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.test_property.id, + "checkin": checkin, + "checkout": checkout, + } + ) + + # ACT + result = self.env["pms.room.type.restriction"].rooms_available( + checkin=checkin, + checkout=checkout, + current_lines=test_reservation.reservation_line_ids.ids, + ) + # ASSERT + obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms) + self.assertTrue( + obtained, + "Availability should contain the test rooms" + "because there's no restriction for them.", + ) + + @Skip + def test_availability_rooms_room_type(self): + # TEST CASE + # get availability withouth restrictions + # given a room type + + # ARRANGE + self.create_common_scenario() + test_rooms_double_rooms = self.env["pms.room"].search( + [ + ("pms_property_id", "=", self.test_property.id), + ("room_type_id", "=", self.test_room_type_double.id), + ] + ) + + # ACT + result = self.env["pms.room.type.restriction"].rooms_available( + checkin=fields.date.today(), + checkout=(fields.datetime.today() + datetime.timedelta(days=4)).date(), + room_type_id=self.test_room_type_double.id, + ) + + # ASSERT + obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms) + self.assertTrue( + obtained, + "Availability should contain the test rooms" + "because there's no restriction for them.", + ) + + @Skip + def test_availability_closed_no_room_type(self): + # TEST CASE: + # coverage for 2 points: + # 1. without room type, restrictions associated with the pricelist are applied + # 2. restriction rule "closed" is taken into account + + # ARRANGE + self.create_common_scenario() + self.test_room_type_restriction1_item1 = self.env[ + "pms.room.type.restriction.item" + ].create( + { + "restriction_id": self.test_room_type_restriction1.id, + "room_type_id": self.test_room_type_double.id, + "date": (fields.datetime.today() + datetime.timedelta(days=2)).date(), + "closed": True, # <- (1/2) + } + ) + # ACT + result = self.env["pms.room.type.restriction"].rooms_available( + checkin=fields.date.today(), + checkout=(fields.datetime.today() + datetime.timedelta(days=4)).date(), + # room_type_id=False, # <- (2/2) + pricelist=self.test_pricelist1.id, + ) + # ASSERT + self.assertNotIn( + self.test_room_type_double, + result.mapped("room_type_id"), + "Availability should not contain rooms of a type " + "which its restriction rules applies", + ) + + @Skip + def test_availability_restrictions(self): + # TEST CASE + # the availability should take into acount restriction rules: + # closed_arrival, closed_departure, min_stay, max_stay, + # min_stay_arrival, max_stay_arrival + + # ARRANGE + self.create_common_scenario() + + self.test_room_type_restriction1_item1 = self.env[ + "pms.room.type.restriction.item" + ].create( + { + "restriction_id": self.test_room_type_restriction1.id, + "room_type_id": self.test_room_type_double.id, + "date": (fields.datetime.today() + datetime.timedelta(days=0)).date(), + } + ) + + checkin = fields.date.today() + checkout = (fields.datetime.today() + datetime.timedelta(days=4)).date() + + test_cases = [ + { + "closed": False, + "closed_arrival": True, + "closed_departure": False, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": True, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": -1, + "date": checkout, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 5, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 0, + "max_stay": 2, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 5, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 3, + "quota": -1, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": 0, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": 0, + "date": checkin, + }, + ] + + for test_case in test_cases: + with self.subTest(k=test_case): + + # ACT + self.test_room_type_restriction1_item1.write(test_case) + + result = self.env["pms.room.type.restriction"].rooms_available( + checkin=checkin, + checkout=checkout, + room_type_id=self.test_room_type_double.id, + pricelist=self.test_pricelist1.id, + ) + + # ASSERT + self.assertNotIn( + self.test_room_type_double, + result.mapped("room_type_id"), + "Availability should not contain rooms of a type " + "which its restriction rules applies", + ) + + @Skip + @freeze_time("1980-11-01") + def test_restriction_on_create_reservation(self): + # TEST CASE + # a restriction should be applied that would prevent the + # creation of reservations + + # ARRANGE + self.create_common_scenario() + self.test_room_type_restriction1_item1 = self.env[ + "pms.room.type.restriction.item" + ].create( + { + "restriction_id": self.test_room_type_restriction1.id, + "room_type_id": self.test_room_type_double.id, + "date": (fields.datetime.today() + datetime.timedelta(days=2)).date(), + "closed": True, + } + ) + + checkin = datetime.datetime.now() + checkout = datetime.datetime.now() + datetime.timedelta(days=4) + + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="Restriction should be applied that would" + " prevent the creation of the reservation.", + ): + self.env["pms.reservation"].create( + { + "pms_property_id": self.test_property.id, + "checkin": checkin, + "checkout": checkout, + "adults": 2, + "room_type_id": self.test_room_type_double.id, + "pricelist_id": self.test_pricelist1.id, + } + ) + + @Skip + @freeze_time("1980-11-01") + def test_restriction_on_create_splitted_reservation(self): + # TEST CASE + # a restriction should be applied that would prevent the + # creation of reservations including splitted reservations. + + # ARRANGE + self.create_common_scenario() + self.test_room_type_restriction1_item1 = self.env[ + "pms.room.type.restriction.item" + ].create( + { + "restriction_id": self.test_room_type_restriction1.id, + "room_type_id": self.test_room_type_double.id, + "date": (fields.datetime.today() + datetime.timedelta(days=2)).date(), + "closed": True, + } + ) + + checkin_test = datetime.datetime.now() + checkout_test = datetime.datetime.now() + datetime.timedelta(days=4) + + self.env["pms.reservation"].create( + { + "pms_property_id": self.test_property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "adults": 2, + "room_type_id": self.test_room_type_double.id, + "preferred_room_id": self.test_room1_double.id, + } + ) + + self.env["pms.reservation"].create( + { + "pms_property_id": self.test_property.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=2), + "checkout": datetime.datetime.now() + datetime.timedelta(days=4), + "adults": 2, + "room_type_id": self.test_room_type_double.id, + "preferred_room_id": self.test_room2_double.id, + } + ) + + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="Restriction should be applied that would" + " prevent the creation of splitted reservation.", + ): + self.env["pms.reservation"].create( + { + "pms_property_id": self.test_property.id, + "checkin": checkin_test, + "checkout": checkout_test, + "adults": 2, + "room_type_id": self.test_room_type_double.id, + "pricelist_id": self.test_pricelist1.id, + } + ) + + @Skip + @freeze_time("1980-11-01") + def test_restriction_update_quota_on_create_reservation(self): + # TEST CASE + # quota rule is changed after creating a reservation + # with pricelist linked to a restriction_item that applies + + # ARRANGE + self.create_common_scenario() + + self.test_room_type_restriction1_item1 = self.env[ + "pms.room.type.restriction.item" + ].create( + { + "restriction_id": self.test_room_type_restriction1.id, + "room_type_id": self.test_room_type_double.id, + "date": datetime.date.today(), + "quota": 1, + } + ) + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.test_property.id, + "checkin": datetime.date.today(), + "checkout": datetime.date.today() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.test_room_type_double.id, + "pricelist_id": self.test_pricelist1.id, + } + ) + r1.flush() + with self.assertRaises( + ValidationError, + msg="The quota shouldnt be enough to create a new reservation", + ): + self.env["pms.reservation"].create( + { + "pms_property_id": self.test_property.id, + "checkin": datetime.date.today(), + "checkout": datetime.date.today() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.test_room_type_double.id, + "pricelist_id": self.test_pricelist1.id, + } + ) + + @freeze_time("1980-11-01") + def test_restriction_update_quota_on_update_reservation(self): + # TEST CASE + # quota rule is restored after creating a reservation + # with pricelist linked to a restriction_item that applies + # and then modify the pricelist of the reservation and + # no restriction applies + + # ARRANGE + self.create_common_scenario() + test_quota = 2 + test_pricelist2 = self.env["product.pricelist"].create( + { + "name": "test pricelist 2", + } + ) + restriction = self.env["pms.room.type.restriction.item"].create( + { + "restriction_id": self.test_room_type_restriction1.id, + "room_type_id": self.test_room_type_double.id, + "date": datetime.date.today(), + "quota": test_quota, + } + ) + reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.test_property.id, + "checkin": datetime.date.today(), + "checkout": datetime.date.today() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.test_room_type_double.id, + "pricelist_id": self.test_pricelist1.id, + } + ) + + # ACT + reservation.pricelist_id = test_pricelist2.id + reservation.flush() + self.assertEqual( + test_quota, + restriction.quota, + "The quota should be restored after changing the reservation's pricelist", + ) diff --git a/pms/views/pms_room_type_restriction_item_views.xml b/pms/views/pms_room_type_restriction_item_views.xml index 2ca2e0b54..da21474cb 100644 --- a/pms/views/pms_room_type_restriction_item_views.xml +++ b/pms/views/pms_room_type_restriction_item_views.xml @@ -7,15 +7,20 @@
- - + + + + + + + diff --git a/pms/views/pms_room_type_restriction_views.xml b/pms/views/pms_room_type_restriction_views.xml index 08aee4290..c9cbbbf78 100644 --- a/pms/views/pms_room_type_restriction_views.xml +++ b/pms/views/pms_room_type_restriction_views.xml @@ -26,21 +26,24 @@ -
+ - - - - - - - - - -
+ +
+ + + + + + + @@ -51,6 +54,12 @@ + +