From ff41cad50cae8ec8543150d59bb9e2fa1e8acd96 Mon Sep 17 00:00:00 2001 From: Dario Lodeiros Date: Tue, 6 Jul 2021 20:04:53 +0200 Subject: [PATCH 1/2] [RFC] Availability and free rooms --- pms/models/pms_availability_plan.py | 203 +-------------- pms/models/pms_property.py | 239 ++++++++++++++++++ pms/models/pms_reservation.py | 28 +- pms/models/pms_reservation_line.py | 8 +- pms/models/pms_room.py | 10 + pms/models/pms_room_type.py | 7 +- pms/tests/test_pms_availability_plan_rules.py | 36 ++- pms/tests/test_pms_booking_engine.py | 2 - pms/wizards/pms_booking_engine.py | 19 +- .../wizard_split_join_swap_reservation.py | 22 +- 10 files changed, 314 insertions(+), 260 deletions(-) diff --git a/pms/models/pms_availability_plan.py b/pms/models/pms_availability_plan.py index 419da062b..df7c20323 100644 --- a/pms/models/pms_availability_plan.py +++ b/pms/models/pms_availability_plan.py @@ -1,10 +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 -from odoo.exceptions import ValidationError -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT +from odoo import api, fields, models class PmsAvailabilityPlan(models.Model): @@ -72,204 +69,6 @@ class PmsAvailabilityPlan(models.Model): ] ) - @api.model - def rooms_available( - self, - checkin, - checkout, - room_type_id=False, - current_lines=False, - pricelist_id=False, - pms_property_id=False, - ): - if current_lines and not isinstance(current_lines, list): - current_lines = [current_lines] - free_rooms = self.get_real_free_rooms( - checkin, checkout, room_type_id, current_lines, pms_property_id - ) - domain_rules = [ - ("date", ">=", checkin), - ( - "date", - "<=", - checkout, - ), # TODO: only closed_departure take account checkout date! - ] - if pms_property_id: - domain_rules.append(("pms_property_id", "=", pms_property_id)) - - if room_type_id: - domain_rules.append(("room_type_id", "=", room_type_id)) - if pricelist_id: - pricelist = self.env["product.pricelist"].browse(pricelist_id) - if pricelist and pricelist.availability_plan_id: - domain_rules.append( - ("availability_plan_id", "=", pricelist.availability_plan_id.id) - ) - rule_items = self.env["pms.availability.plan.rule"].search(domain_rules) - - if len(rule_items) > 0: - room_types_to_remove = [] - for item in rule_items: - if self.any_rule_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 - ) - elif not pricelist: - raise ValidationError(_("Pricelist not found")) - return free_rooms.sorted(key=lambda r: r.sequence) - - def get_real_free_rooms( - self, - checkin, - checkout, - room_type_id=False, - current_lines=False, - pms_property_id=False, - ): - Avail = self.env["pms.availability"] - if isinstance(checkin, str): - checkin = datetime.datetime.strptime( - checkin, DEFAULT_SERVER_DATE_FORMAT - ).date() - if isinstance(checkout, str): - checkout = datetime.datetime.strptime( - checkout, DEFAULT_SERVER_DATE_FORMAT - ).date() - domain = [ - ("date", ">=", checkin), - ("date", "<=", checkout - datetime.timedelta(1)), - ] - if not current_lines: - current_lines = [] - rooms_not_avail = ( - Avail.search(domain) - .reservation_line_ids.filtered( - lambda l: l.occupies_availability and l.id and l.id not in current_lines - ) - .room_id.ids - ) - domain_rooms = [] - if rooms_not_avail: - domain_rooms = [ - ("id", "not in", rooms_not_avail), - ] - if pms_property_id: - domain_rooms.append(("pms_property_id", "=", pms_property_id)) - if room_type_id: - domain_rooms.append(("room_type_id", "=", room_type_id)) - return self.env["pms.room"].search(domain_rooms) - - @api.model - def get_count_rooms_available( - self, - checkin, - checkout, - room_type_id, - pms_property_id, - current_lines=False, - pricelist_id=False, - ): - if current_lines and not isinstance(current_lines, list): - current_lines = [current_lines] - - avail = self.get_count_real_free_rooms( - checkin, checkout, room_type_id, pms_property_id, current_lines - ) - domain_rules = [ - ("date", ">=", checkin), - ( - "date", - "<=", - checkout, - ), # TODO: only closed_departure take account checkout date! - ("room_type_id", "=", room_type_id), - ("pms_property_id", "=", pms_property_id), - ] - pricelist = False - if pricelist_id: - pricelist = self.env["product.pricelist"].browse(pricelist_id) - if pricelist and pricelist.availability_plan_id: - domain_rules.append( - ("availability_plan_id", "=", pricelist.availability_plan_id.id) - ) - rule_items = self.env["pms.availability.plan.rule"].search(domain_rules) - if len(rule_items) > 0: - for item in rule_items: - if self.any_rule_applies(checkin, checkout, item): - return 0 - avail = min(rule_items.mapped("plan_avail")) - return avail - - def get_count_real_free_rooms( - self, - checkin, - checkout, - room_type_id, - pms_property_id, - current_lines=False, - ): - Avail = self.env["pms.availability"] - count_free_rooms = len( - self.env["pms.room.type"] - .browse(room_type_id) - .room_ids.filtered(lambda r: r.pms_property_id.id == pms_property_id) - ) - if isinstance(checkin, str): - checkin = datetime.datetime.strptime( - checkin, DEFAULT_SERVER_DATE_FORMAT - ).date() - if isinstance(checkout, str): - checkout = datetime.datetime.strptime( - checkout, DEFAULT_SERVER_DATE_FORMAT - ).date() - for avail in Avail.search( - [ - ("date", ">=", checkin), - ("date", "<=", checkout - datetime.timedelta(1)), - ("room_type_id", "=", room_type_id), - ("pms_property_id", "=", pms_property_id), - ] - ): - if avail.real_avail < count_free_rooms: - count_free_rooms = avail.real_avail - return count_free_rooms - - @api.model - def splitted_availability( - self, - checkin, - checkout, - room_type_id=False, - current_lines=False, - pricelist=False, - pms_property_id=False, - ): - if isinstance(checkin, str): - checkin = datetime.datetime.strptime( - checkin, DEFAULT_SERVER_DATE_FORMAT - ).date() - if isinstance(checkout, str): - checkout = datetime.datetime.strptime( - checkout, DEFAULT_SERVER_DATE_FORMAT - ).date() - 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_id=pricelist.id, - pms_property_id=pms_property_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: diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index e6609ee31..8d63c957f 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -2,12 +2,14 @@ # Copyright 2019 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import datetime import time import pytz from odoo import _, api, fields, models from odoo.exceptions import ValidationError +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT from odoo.addons.base.models.res_partner import _tz_get @@ -97,6 +99,243 @@ class PmsProperty(models.Model): "charge a day's stay according to current rate that day", help="Notice under the signature on the traveler's ticket.", ) + free_room_ids = fields.One2many( + string="Rooms available", + help="allows you to send different parameters in the context " + "(checkin(required), checkout(required), room_type_id, location_id, capacity, " + "amenity_ids and / or pricelist_id) and return rooms available", + comodel_name="pms.room", + compute="_compute_free_room_ids", + ) + availability = fields.Integer( + string="Number of rooms available", + help="allows you to send different parameters in the context " + "(checkin(required), checkout(required), room_type_id, location_id, capacity," + "amenity_ids and / or pricelist_id) check the availability for the hotel", + compute="_compute_availability", + ) + + @api.depends_context( + "checkin", + "checkout", + "room_type_id", + "location_id", + "capacity", + "amenity_ids", + "pricelist_id", + "current_lines", + ) + def _compute_free_room_ids(self): + self.free_room_ids = False + checkin = self._context["checkin"] + checkout = self._context["checkout"] + if isinstance(checkin, str): + checkin = datetime.datetime.strptime( + checkin, DEFAULT_SERVER_DATE_FORMAT + ).date() + if isinstance(checkout, str): + checkout = datetime.datetime.strptime( + checkout, DEFAULT_SERVER_DATE_FORMAT + ).date() + current_lines = self.env.context.get("current_lines", False) + if current_lines and not isinstance(current_lines, list): + current_lines = [current_lines] + + 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 + ) + if pricelist_id: + # TODO: only closed_departure take account checkout date! + domain_rules = [ + ("date", ">=", checkin), + ("date", "<=", checkout), + ("pms_property_id", "=", pms_property.id), + ] + if room_type_id: + domain_rules.append(("room_type_id", "=", room_type_id)) + + pricelist = self.env["product.pricelist"].browse(pricelist_id) + if pricelist.availability_plan_id: + domain_rules.append( + ("availability_plan_id", "=", pricelist.availability_plan_id.id) + ) + rule_items = self.env["pms.availability.plan.rule"].search( + domain_rules + ) + + if len(rule_items) > 0: + room_types_to_remove = [] + for item in rule_items: + if pricelist.availability_plan_id.any_rule_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 + ) + if len(free_rooms) > 0: + pms_property.free_room_ids = free_rooms.ids + else: + pms_property.free_room_ids = False + + def get_real_free_rooms(self, checkin, checkout, current_lines=False): + self.ensure_one() + Avail = self.env["pms.availability"] + tarjet_rooms = self.env["pms.room"].search([("pms_property_id", "=", self.id)]) + + room_type_id = self.env.context.get("room_type_id", False) + if room_type_id: + tarjet_rooms = tarjet_rooms.filtered( + lambda r: r.room_type_id.id == room_type_id + ) + + capacity = self.env.context.get("capacity", False) + if capacity: + tarjet_rooms = tarjet_rooms.filtered(lambda r: r.capacity >= capacity) + + location_id = self.env.context.get("location_id", False) + if location_id: + tarjet_rooms = tarjet_rooms.filtered( + lambda r: r.location_id.id == location_id + ) + + amenity_ids = self.env.context.get("amenity_ids", False) + if amenity_ids: + if amenity_ids and not isinstance(amenity_ids, list): + amenity_ids = [amenity_ids] + tarjet_rooms = tarjet_rooms.filtered( + 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 + ) + + domain_rooms = [("id", "in", tarjet_rooms.ids)] + if rooms_not_avail: + domain_rooms.append( + ("id", "not in", rooms_not_avail), + ) + return self.env["pms.room"].search(domain_rooms) + + @api.depends_context( + "checkin", + "checkout", + "room_type_id", + "location_id", + "capacity", + "amenity_ids", + "pricelist_id", + "current_lines", + ) + def _compute_availability(self): + self.availability = 0 + for pms_property in self: + checkin = self._context["checkin"] + checkout = self._context["checkout"] + 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) + pms_property = pms_property.with_context( + checkin=checkin, + checkout=checkout, + room_type_id=room_type_id, + current_lines=current_lines, + 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] + + domain_rules = [ + ("date", ">=", checkin), + ("date", "<=", checkout), + ("pms_property_id", "=", pms_property.id), + ] + + if room_type_id: + domain_rules.append(("room_type_id", "=", room_type_id)) + + pricelist = False + if pricelist_id: + pricelist = self.env["product.pricelist"].browse(pricelist_id) + if pricelist and pricelist.availability_plan_id: + domain_rules.append( + ("availability_plan_id", "=", pricelist.availability_plan_id.id) + ) + rule_groups = self.env["pms.availability.plan.rule"].read_group( + domain_rules, + ["plan_avail:sum"], + ["date:day"], + lazy=False, + ) + if len(rule_groups) > 0: + # If in the group per day, some room type has the sale blocked, + # we must subtract from that day the availability of that room type + for group in rule_groups: + items = self.env["pms.availability.plan.rule"].search( + group["__domain"] + ) + for item in items: + if pricelist.availability_plan_id.any_rule_applies( + checkin, checkout, item + ): + group["plan_avail"] -= item.plan_avail + count_free_rooms = min(i["plan_avail"] for i in rule_groups) + pms_property.availability = count_free_rooms + + @api.model + def splitted_availability( + self, + checkin, + checkout, + pms_property_id, + room_type_id=False, + current_lines=False, + pricelist=False, + ): + if isinstance(checkin, str): + checkin = datetime.datetime.strptime( + checkin, DEFAULT_SERVER_DATE_FORMAT + ).date() + if isinstance(checkout, str): + checkout = datetime.datetime.strptime( + checkout, DEFAULT_SERVER_DATE_FORMAT + ).date() + for date_iterator in [ + checkin + datetime.timedelta(days=x) + for x in range(0, (checkout - checkin).days) + ]: + pms_property = self.env["pms.property"].browse(pms_property_id) + pms_property = pms_property.with_context( + checkin=date_iterator, + checkout=date_iterator + datetime.timedelta(1), + room_type_id=room_type_id, + current_lines=current_lines, + pricelist_id=pricelist.id, + ) + + if len(pms_property.free_room_ids) < 1: + return False + return True @api.constrains("default_arrival_hour") def _check_arrival_hour(self): diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 542cafa1f..91a882b1b 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -736,15 +736,16 @@ class PmsReservation(models.Model): [("active", "=", True)] ) return - rooms_available = self.env["pms.availability.plan"].rooms_available( + pms_property = reservation.pms_property_id + pms_property = pms_property.with_context( checkin=reservation.checkin, checkout=reservation.checkout, room_type_id=False, # Allows to choose any available room current_lines=reservation.reservation_line_ids.ids, pricelist_id=reservation.pricelist_id.id, - pms_property_id=reservation.pms_property_id.id, ) - reservation.allowed_room_ids = rooms_available + reservation.allowed_room_ids = pms_property.free_room_ids + else: reservation.allowed_room_ids = False @@ -1502,13 +1503,15 @@ class PmsReservation(models.Model): return self.folio_id.action_pay() def open_reservation_wizard(self): - rooms_available = self.env["pms.availability.plan"].rooms_available( + pms_property = self.pms_property_id + pms_property = pms_property.with_context( checkin=self.checkin, checkout=self.checkout, current_lines=self.reservation_line_ids.ids, pricelist_id=self.pricelist_id.id, - pms_property_id=self.pms_property_id.id, ) + rooms_available = pms_property.free_room_ids + # REVIEW: check capacity room return { "view_type": "form", @@ -1544,21 +1547,6 @@ class PmsReservation(models.Model): result.append((res.id, name)) return result - # REVIEW: Is it necessary? - def copy_data(self, default=None): - rooms_available = self.env["pms.availability.plan"].rooms_available( - self.checkin, - self.checkout, - room_type_id=self.room_type_id.id, - pricelist_id=self.pricelist_id.id, - pms_property_id=self.pms_property_id.id, - ) - if self.preferred_room_id.id in rooms_available.ids: - default["preferred_room_id"] = self.preferred_room_id.id - if self.room_type_id.id in rooms_available.mapped("room_type_id.id"): - default["room_type_id"] = self.room_type_id.id - return super(PmsReservation, self).copy_data(default) - @api.model def create(self, vals): if vals.get("folio_id"): diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index fa9623229..df504aa32 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -175,7 +175,8 @@ class PmsReservationLine(models.Model): 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.availability.plan"].rooms_available( + pms_property = line.pms_property_id + pms_property = pms_property.with_context( checkin=reservation.checkin, checkout=reservation.checkout, room_type_id=reservation.room_type_id.id @@ -183,8 +184,9 @@ class PmsReservationLine(models.Model): else False, current_lines=reservation.reservation_line_ids.ids, pricelist_id=reservation.pricelist_id.id, - pms_property_id=line.pms_property_id.id, ) + rooms_available = pms_property.free_room_ids + # Check if the room assigment is manual or automatic to set the # to_assign value on reservation manual_assigned = False @@ -228,7 +230,7 @@ class PmsReservationLine(models.Model): else: line.room_id = rooms_available[0] # check that the reservation cannot be allocated even by dividing it - elif not self.env["pms.availability.plan"].splitted_availability( + elif not self.env["pms.property"].splitted_availability( checkin=reservation.checkin, checkout=reservation.checkout, room_type_id=reservation.room_type_id.id, diff --git a/pms/models/pms_room.py b/pms/models/pms_room.py index 5511930cc..e83742eac 100644 --- a/pms/models/pms_room.py +++ b/pms/models/pms_room.py @@ -70,6 +70,16 @@ class PmsRoom(models.Model): required=True, default="0", ) + room_amenity_ids = fields.Many2many( + string="Room Amenities", + help="List of amenities included in room", + comodel_name="pms.amenity", + relation="pms_room_amenity_rel", + column1="room_id", + column2="amenity_id", + check_pms_properties=True, + ) + description_sale = fields.Text( string="Sale Description", help="A description of the Product that you want to communicate to " diff --git a/pms/models/pms_room_type.py b/pms/models/pms_room_type.py index b097cbbd1..076465651 100644 --- a/pms/models/pms_room_type.py +++ b/pms/models/pms_room_type.py @@ -98,13 +98,16 @@ class PmsRoomType(models.Model): for room_type in self: name = room_type.name if self._context.get("checkin") and self._context.get("checkout"): - avail = self.env["pms.availability.plan"].get_count_rooms_available( + pms_property = self.env["pms.property"].browse( + self._context.get("pms_property_id") + ) + pms_property = pms_property.with_context( checkin=self._context.get("checkin"), checkout=self._context.get("checkout"), room_type_id=room_type.id, - pms_property_id=self._context.get("pms_property_id") or False, pricelist_id=self._context.get("pricelist_id") or False, ) + avail = pms_property.availability name += " (%s)" % avail result.append((room_type.id, name)) return result diff --git a/pms/tests/test_pms_availability_plan_rules.py b/pms/tests/test_pms_availability_plan_rules.py index c4b753a2f..d46fa8ba7 100644 --- a/pms/tests/test_pms_availability_plan_rules.py +++ b/pms/tests/test_pms_availability_plan_rules.py @@ -130,7 +130,7 @@ class TestPmsRoomTypeAvailabilityRules(TestPms): --------------------- The checkin and checkout dates on which the availability will be checked are saved in a variable and in another all the rooms of the property are also saved. Then the - rooms_available() method is launched which should return the number of available rooms + free_room_ids compute field is called which should return the number of available rooms of the property and they are saved in another variable with which it is verified that all the rooms have been returned because there are no availability rules for that plan. """ @@ -142,10 +142,12 @@ class TestPmsRoomTypeAvailabilityRules(TestPms): [("pms_property_id", "=", self.pms_property3.id)] ) # ACT - result = self.env["pms.availability.plan"].rooms_available( + pms_property = self.pms_property3.with_context( checkin=checkin, checkout=checkout, ) + result = pms_property.free_room_ids + # ASSERT obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms) self.assertTrue( @@ -162,7 +164,7 @@ class TestPmsRoomTypeAvailabilityRules(TestPms): ----------------- The checkin and checkout dates on which the availability will be checked are saved in a variable and in another all the rooms of the property are also saved. Then create - a reservation for this property and the rooms_available() method is launched with the + a reservation for this property and the free_room_ids compute field is called with the parameters checkin, checkout and the reservation lines of the reservation as a curent lines, this method should return the number of available rooms of the property. Then the result is saved in another variable with which it is verified that all the rooms have @@ -185,11 +187,13 @@ class TestPmsRoomTypeAvailabilityRules(TestPms): ) # ACT - result = self.env["pms.availability.plan"].rooms_available( + pms_property = self.pms_property3.with_context( checkin=checkin, checkout=checkout, current_lines=test_reservation.reservation_line_ids.ids, ) + result = pms_property.free_room_ids + # ASSERT obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms) self.assertTrue( @@ -202,8 +206,8 @@ class TestPmsRoomTypeAvailabilityRules(TestPms): """ Check the availability of a room type for a property. ---------------- - Double rooms of a property are saved in a variable. The rooms_available() method - is launched giving as parameters checkin, checkout and the type of room (in this + Double rooms of a property are saved in a variable. The free_room_ids compute field + is called giving as parameters checkin, checkout and the type of room (in this case double). Then with the all () function we check that all rooms of this type were returned. """ @@ -216,11 +220,12 @@ class TestPmsRoomTypeAvailabilityRules(TestPms): ] ) # ACT - result = self.env["pms.availability.plan"].rooms_available( + pms_property = self.pms_property3.with_context( checkin=fields.date.today(), checkout=(fields.datetime.today() + datetime.timedelta(days=4)).date(), room_type_id=self.test_room_type_double.id, ) + result = pms_property.free_room_ids # ASSERT obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms) @@ -237,7 +242,7 @@ class TestPmsRoomTypeAvailabilityRules(TestPms): -------------------- Create an availability rule for double rooms with the field closed = true and the date from today until tomorrow. Then the availability is saved in a - variable through the rooms_available() method, passing it the pricelist that + variable through the free_room_ids computed field, passing it the pricelist that it contains the availability plan where the rule is included, and the checkin and checkout dates are between the date of the rule. Then it is verified that the double rooms are not available. @@ -255,12 +260,14 @@ class TestPmsRoomTypeAvailabilityRules(TestPms): } ) # ACT - result = self.env["pms.availability.plan"].rooms_available( + pms_property = self.pms_property3.with_context( checkin=fields.date.today(), checkout=(fields.datetime.today() + datetime.timedelta(days=4)).date(), # room_type_id=False, # <- (2/2) pricelist_id=self.pricelist2.id, ) + result = pms_property.free_room_ids + # ASSERT self.assertNotIn( self.test_room_type_double, @@ -283,7 +290,7 @@ class TestPmsRoomTypeAvailabilityRules(TestPms): 6. max_stay_arrival = 3 7. quota = 0 8. max_avail = 0 - For each test case, it is verified through the rooms_available() method, + For each test case, it is verified through the free_room_ids compute field, that double rooms are not available since the rules are applied to this room type. """ @@ -409,12 +416,13 @@ class TestPmsRoomTypeAvailabilityRules(TestPms): # ACT self.test_room_type_availability_rule1.write(test_case) - result = self.env["pms.availability.plan"].rooms_available( + pms_property = self.pms_property3.with_context( checkin=checkin, checkout=checkout, room_type_id=self.test_room_type_double.id, pricelist_id=self.pricelist2.id, ) + result = pms_property.free_room_ids # ASSERT self.assertNotIn( @@ -639,15 +647,17 @@ class TestPmsRoomTypeAvailabilityRules(TestPms): for p in properties: with self.subTest(k=p): # ACT - rooms_avail = self.env["pms.availability.plan"].rooms_available( + pms_property = self.env["pms.property"].browse(p["property"]) + pms_property = pms_property.with_context( checkin=fields.date.today(), checkout=( fields.datetime.today() + datetime.timedelta(days=2) ).date(), room_type_id=self.test_room_type_special.id, pricelist_id=self.pricelist2.id, - pms_property_id=p["property"], ) + rooms_avail = pms_property.free_room_ids + # ASSERT self.assertEqual( len(rooms_avail) > 0, p["value"], "Availability is not correct" diff --git a/pms/tests/test_pms_booking_engine.py b/pms/tests/test_pms_booking_engine.py index 7f4bc3bce..3bcfe9c3e 100644 --- a/pms/tests/test_pms_booking_engine.py +++ b/pms/tests/test_pms_booking_engine.py @@ -703,7 +703,6 @@ class TestPmsWizardMassiveChanges(common.SavepointCase): "pms_property_id": self.test_property.id, } ) - # create folio wizard with partner id => pricelist & start-end dates booking_engine = self.env["pms.booking.engine"].create( { @@ -715,7 +714,6 @@ class TestPmsWizardMassiveChanges(common.SavepointCase): } ) booking_engine.flush() - room_type_plan_avail = booking_engine.availability_results.filtered( lambda r: r.room_type_id.id == self.test_room_type_double.id ).num_rooms_available diff --git a/pms/wizards/pms_booking_engine.py b/pms/wizards/pms_booking_engine.py index 6366d41b3..094424d94 100644 --- a/pms/wizards/pms_booking_engine.py +++ b/pms/wizards/pms_booking_engine.py @@ -177,15 +177,15 @@ class BookingEngine(models.TransientModel): ("pms_property_ids", "in", record.pms_property_id.id), ] ): - num_rooms_available = self.env[ - "pms.availability.plan" - ].get_count_rooms_available( + pms_property = record.pms_property_id + pms_property = pms_property.with_context( checkin=record.start_date, checkout=record.end_date, room_type_id=room_type_iterator.id, pricelist_id=record.pricelist_id.id, - pms_property_id=record.pms_property_id.id, ) + num_rooms_available = pms_property.availability + cmds.append( ( 0, @@ -338,15 +338,14 @@ class AvailabilityWizard(models.TransientModel): @api.depends("room_type_id", "checkin", "checkout") def _compute_num_rooms_available(self): for record in self: - record.num_rooms_available = self.env[ - "pms.availability.plan" - ].get_count_rooms_available( - record.checkin, - record.checkout, + pms_property = record.booking_engine_id.pms_property_id + pms_property = pms_property.with_context( + checkin=record.checkin, + checkout=record.checkout, room_type_id=record.room_type_id.id, pricelist_id=record.booking_engine_id.pricelist_id.id, - pms_property_id=record.booking_engine_id.pms_property_id.id, ) + record.num_rooms_available = pms_property.availability @api.depends("num_rooms_available") def _compute_num_rooms_selected(self): diff --git a/pms/wizards/wizard_split_join_swap_reservation.py b/pms/wizards/wizard_split_join_swap_reservation.py index b2d247ce5..53801a1e1 100644 --- a/pms/wizards/wizard_split_join_swap_reservation.py +++ b/pms/wizards/wizard_split_join_swap_reservation.py @@ -172,14 +172,16 @@ class ReservationSplitJoinSwapWizard(models.TransientModel): record.allowed_rooms_target = False record.room_target = False if record.checkin and record.checkout: - rooms_available = self.env["pms.availability.plan"].rooms_available( + pms_property = record.reservation_id.pms_property_id + pms_property = pms_property.with_context( checkin=record.checkin, checkout=record.checkout, room_type_id=False, # Allows to choose any available room current_lines=record.reservation_id.reservation_line_ids.ids, pricelist_id=record.reservation_id.pricelist_id.id, - pms_property_id=record.reservation_id.pms_property_id.id, ) + rooms_available = pms_property.free_room_ids + domain = [("capacity", ">=", record.reservation_id.adults)] if record.room_source: domain.append(("id", "!=", record.room_source.id)) @@ -214,7 +216,7 @@ class ReservationSplitJoinSwapWizard(models.TransientModel): if not self.browse(room.id): raise UserError(_("The room does not exist")) - rooms_available = self.env["pms.availability.plan"].rooms_available( + pms_property = reservation.pms_property_id.with_context( checkin=date, checkout=( datetime.datetime(year=date.year, month=date.month, day=date.day) @@ -222,8 +224,9 @@ class ReservationSplitJoinSwapWizard(models.TransientModel): ).date(), current_lines=reservation.reservation_line_ids.ids, pricelist_id=reservation.pricelist_id.id, - pms_property_id=reservation.pms_property_id.id, ) + rooms_available = pms_property.free_room_ids + if room not in rooms_available: raise UserError(_("The room is not available")) @@ -233,13 +236,15 @@ class ReservationSplitJoinSwapWizard(models.TransientModel): @api.model def reservation_join(self, reservation, room): - rooms_available = self.env["pms.availability.plan"].rooms_available( + pms_property = reservation.pms_property_id + pms_property = pms_property.with_context( checkin=reservation.checkin, checkout=reservation.checkout, current_lines=reservation.reservation_line_ids.ids, pricelist_id=reservation.pricelist_id.id, - pms_property_id=reservation.pms_property_id.id, ) + rooms_available = pms_property.free_room_ids + if room in rooms_available: for line in ( self.env["pms.reservation"] @@ -347,13 +352,14 @@ class ReservationLinesToSplit(models.TransientModel): [("active", "=", True)] ) return - rooms_available = self.env["pms.availability.plan"].rooms_available( + pms_property = reservation.pms_property_id + pms_property = pms_property.with_context( checkin=line.date, checkout=line.date + datetime.timedelta(days=1), room_type_id=False, # Allows to choose any available room pricelist_id=reservation.pricelist_id.id, - pms_property_id=reservation.pms_property_id.id, ) + rooms_available = pms_property.free_room_ids rooms_available += line.room_id line.allowed_room_ids = rooms_available else: From d6461b0bef4a8eac1a4fabfd7f8750933b6458da Mon Sep 17 00:00:00 2001 From: Dario Lodeiros Date: Tue, 6 Jul 2021 23:46:52 +0200 Subject: [PATCH 2/2] [FIX] compute field availability cache --- pms/models/pms_availability_plan.py | 11 +++ pms/models/pms_property.py | 141 ++++++++++++++-------------- 2 files changed, 84 insertions(+), 68 deletions(-) diff --git a/pms/models/pms_availability_plan.py b/pms/models/pms_availability_plan.py index df7c20323..76b16ba83 100644 --- a/pms/models/pms_availability_plan.py +++ b/pms/models/pms_availability_plan.py @@ -1,7 +1,10 @@ # 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 +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT class PmsAvailabilityPlan(models.Model): @@ -55,6 +58,14 @@ class PmsAvailabilityPlan(models.Model): @classmethod def any_rule_applies(cls, checkin, checkout, item): + if isinstance(checkin, str): + checkin = datetime.datetime.strptime( + checkin, DEFAULT_SERVER_DATE_FORMAT + ).date() + if isinstance(checkout, str): + checkout = datetime.datetime.strptime( + checkout, DEFAULT_SERVER_DATE_FORMAT + ).date() reservation_len = (checkout - checkin).days return any( [ diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index 8d63c957f..37f30f759 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -102,7 +102,7 @@ class PmsProperty(models.Model): free_room_ids = fields.One2many( string="Rooms available", help="allows you to send different parameters in the context " - "(checkin(required), checkout(required), room_type_id, location_id, capacity, " + "(checkin(required), checkout(required), room_type_id, ubication_id, capacity, " "amenity_ids and / or pricelist_id) and return rooms available", comodel_name="pms.room", compute="_compute_free_room_ids", @@ -110,7 +110,7 @@ class PmsProperty(models.Model): availability = fields.Integer( string="Number of rooms available", help="allows you to send different parameters in the context " - "(checkin(required), checkout(required), room_type_id, location_id, capacity," + "(checkin(required), checkout(required), room_type_id, ubication_id, capacity," "amenity_ids and / or pricelist_id) check the availability for the hotel", compute="_compute_availability", ) @@ -119,14 +119,13 @@ class PmsProperty(models.Model): "checkin", "checkout", "room_type_id", - "location_id", + "ubication_id", "capacity", "amenity_ids", "pricelist_id", "current_lines", ) def _compute_free_room_ids(self): - self.free_room_ids = False checkin = self._context["checkin"] checkout = self._context["checkout"] if isinstance(checkin, str): @@ -185,29 +184,29 @@ class PmsProperty(models.Model): def get_real_free_rooms(self, checkin, checkout, current_lines=False): self.ensure_one() Avail = self.env["pms.availability"] - tarjet_rooms = self.env["pms.room"].search([("pms_property_id", "=", self.id)]) + target_rooms = self.env["pms.room"].search([("pms_property_id", "=", self.id)]) room_type_id = self.env.context.get("room_type_id", False) if room_type_id: - tarjet_rooms = tarjet_rooms.filtered( + target_rooms = target_rooms.filtered( lambda r: r.room_type_id.id == room_type_id ) capacity = self.env.context.get("capacity", False) if capacity: - tarjet_rooms = tarjet_rooms.filtered(lambda r: r.capacity >= capacity) + target_rooms = target_rooms.filtered(lambda r: r.capacity >= capacity) - location_id = self.env.context.get("location_id", False) - if location_id: - tarjet_rooms = tarjet_rooms.filtered( - lambda r: r.location_id.id == location_id + ubication_id = self.env.context.get("ubication_id", False) + if ubication_id: + target_rooms = target_rooms.filtered( + lambda r: r.ubication_id.id == ubication_id ) amenity_ids = self.env.context.get("amenity_ids", False) if amenity_ids: if amenity_ids and not isinstance(amenity_ids, list): amenity_ids = [amenity_ids] - tarjet_rooms = tarjet_rooms.filtered( + target_rooms = target_rooms.filtered( lambda r: len(set(amenity_ids) - set(r.room_amenity_ids.ids)) == 0 ) @@ -228,7 +227,7 @@ class PmsProperty(models.Model): .room_id.ids ) - domain_rooms = [("id", "in", tarjet_rooms.ids)] + domain_rooms = [("id", "in", target_rooms.ids)] if rooms_not_avail: domain_rooms.append( ("id", "not in", rooms_not_avail), @@ -239,68 +238,74 @@ class PmsProperty(models.Model): "checkin", "checkout", "room_type_id", - "location_id", + "ubication_id", "capacity", "amenity_ids", "pricelist_id", "current_lines", ) def _compute_availability(self): - self.availability = 0 - for pms_property in self: - checkin = self._context["checkin"] - checkout = self._context["checkout"] - 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) - pms_property = pms_property.with_context( - checkin=checkin, - checkout=checkout, - room_type_id=room_type_id, - current_lines=current_lines, - pricelist_id=pricelist_id, + self.ensure_one() + checkin = self._context["checkin"] + checkout = self._context["checkout"] + if isinstance(checkin, str): + checkin = datetime.datetime.strptime( + checkin, DEFAULT_SERVER_DATE_FORMAT + ).date() + if isinstance(checkout, str): + checkout = datetime.datetime.strptime( + checkout, DEFAULT_SERVER_DATE_FORMAT + ).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) + pms_property = self.with_context( + checkin=checkin, + checkout=checkout, + room_type_id=room_type_id, + current_lines=current_lines, + 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] + + domain_rules = [ + ("date", ">=", checkin), + ("date", "<=", checkout), + ("pms_property_id", "=", pms_property.id), + ] + if room_type_id: + domain_rules.append(("room_type_id", "=", room_type_id)) + + pricelist = False + if pricelist_id: + pricelist = self.env["product.pricelist"].browse(pricelist_id) + if pricelist and pricelist.availability_plan_id: + domain_rules.append( + ("availability_plan_id", "=", pricelist.availability_plan_id.id) ) - count_free_rooms = len(pms_property.free_room_ids) - - if current_lines and not isinstance(current_lines, list): - current_lines = [current_lines] - - domain_rules = [ - ("date", ">=", checkin), - ("date", "<=", checkout), - ("pms_property_id", "=", pms_property.id), - ] - - if room_type_id: - domain_rules.append(("room_type_id", "=", room_type_id)) - - pricelist = False - if pricelist_id: - pricelist = self.env["product.pricelist"].browse(pricelist_id) - if pricelist and pricelist.availability_plan_id: - domain_rules.append( - ("availability_plan_id", "=", pricelist.availability_plan_id.id) - ) - rule_groups = self.env["pms.availability.plan.rule"].read_group( - domain_rules, - ["plan_avail:sum"], - ["date:day"], - lazy=False, - ) - if len(rule_groups) > 0: - # If in the group per day, some room type has the sale blocked, - # we must subtract from that day the availability of that room type - for group in rule_groups: - items = self.env["pms.availability.plan.rule"].search( - group["__domain"] - ) - for item in items: - if pricelist.availability_plan_id.any_rule_applies( - checkin, checkout, item - ): - group["plan_avail"] -= item.plan_avail - count_free_rooms = min(i["plan_avail"] for i in rule_groups) - pms_property.availability = count_free_rooms + rule_groups = self.env["pms.availability.plan.rule"].read_group( + domain_rules, + ["plan_avail:sum"], + ["date:day"], + lazy=False, + ) + if len(rule_groups) > 0: + # If in the group per day, some room type has the sale blocked, + # we must subtract from that day the availability of that room type + for group in rule_groups: + items = self.env["pms.availability.plan.rule"].search( + group["__domain"] + ) + for item in items: + if pricelist.availability_plan_id.any_rule_applies( + checkin, checkout, item + ): + group["plan_avail"] -= item.plan_avail + count_free_rooms = min(i["plan_avail"] for i in rule_groups) + self.availability = count_free_rooms @api.model def splitted_availability(