diff --git a/pms/models/__init__.py b/pms/models/__init__.py index e3401835e..de21c994f 100644 --- a/pms/models/__init__.py +++ b/pms/models/__init__.py @@ -20,6 +20,7 @@ from . import account_move from . import product_template from . import res_company from . import account_payment +from . import pms_room_type_availability from . import pms_room_type_restriction from . import pms_room_type_restriction_item from . import pms_reservation_line diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index bb61a96b6..a73d80141 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -857,12 +857,12 @@ class PmsFolio(models.Model): and not rline.parent_reservation and rline.state == state ): - dates = (rline.real_checkin, rline.real_checkout) + dates = (rline.checkin, rline.checkout) vals = { "num": len( self.reservation_ids.filtered( - lambda r: r.real_checkin == dates[0] - and r.real_checkout == dates[1] + lambda r: r.checkin == dates[0] + and r.checkout == dates[1] and r.room_type_id.id == rline.room_type_id.id and (r.to_send or import_all) and not r.parent_reservation diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 5dcf63494..b12d1e07c 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -244,8 +244,6 @@ class PmsReservation(models.Model): out_service_description = fields.Text("Cause of out of service") checkin = fields.Date("Check In", required=True, default=_get_default_checkin) checkout = fields.Date("Check Out", required=True, default=_get_default_checkout) - real_checkin = fields.Date("From", compute="_compute_real_checkin", store=True,) - real_checkout = fields.Date("To", compute="_compute_real_checkout", store=True,) arrival_hour = fields.Char( "Arrival Hour", default=_get_default_arrival_hour, @@ -460,12 +458,12 @@ class PmsReservation(models.Model): ) return rooms_available = ( - self.env["pms.room.type"].check_availability_room_type( - dfrom=reservation.checkin, - dto=reservation.checkout, + self.env["pms.room.type.availability"].rooms_available( + checkin=reservation.checkin, + checkout=reservation.checkout, room_type_id=False, # Allow chosen any available room + current_lines=reservation.reservation_line_ids.ids, ) - + self.room_id ) if ( reservation.room_id @@ -572,65 +570,6 @@ class PmsReservation(models.Model): else: reservation.adults = 0 - @api.depends("checkin") - def _compute_real_checkin(self): - for reservation in self: - reservation.real_checkin = reservation.checkin - if reservation.splitted: - master_reservation = reservation.parent_reservation or reservation - reservation.real_checkin = master_reservation.checkin - splitted_reservations = self.env["pms.reservation"].search( - [ - ("splitted", "=", True), - ("folio_id", "=", self.folio_id.id), - "|", - ("parent_reservation", "=", master_reservation.id), - ("id", "=", master_reservation.id), - ] - ) - for split in splitted_reservations: - if reservation.real_checkin > split.checkin: - reservation.real_checkin = split.checkin - - @api.depends("checkout") - def _compute_real_checkout(self): - for reservation in self: - reservation.real_checkout = reservation.checkout - if reservation.splitted: - master_reservation = reservation.parent_reservation or reservation - reservation.real_checkout = master_reservation.checkout - splitted_reservations = self.env["pms.reservation"].search( - [ - ("splitted", "=", True), - ("folio_id", "=", self.folio_id.id), - "|", - ("parent_reservation", "=", master_reservation.id), - ("id", "=", master_reservation.id), - ] - ) - for split in splitted_reservations: - if reservation.real_checkout < split.checkout: - reservation.real_checkout = split.checkout - - @api.depends("splitted", "checkout") - def _compute_real_checkin(self): - for reservation in self: - if reservation.splitted: - master_reservation = reservation.parent_reservation or reservation - first_checkin = master_reservation.checkin - splitted_reservations = self.env["pms.reservation"].search( - [ - ("splitted", "=", True), - ("folio_id", "=", self.folio_id.id), - "|", - ("parent_reservation", "=", master_reservation.id), - ("id", "=", master_reservation.id), - ] - ) - for split in splitted_reservations: - if first_checkin > split.checkin: - reservation.real_checkin = split.checkin - @api.depends("checkin", "checkout", "state") def _compute_to_send(self): for reservation in self: @@ -987,9 +926,9 @@ class PmsReservation(models.Model): def _autoassign(self): self.ensure_one() room_chosen = False - rooms_available = self.env["pms.room.type"].check_availability_room_type( - dfrom=self.checkin, - dto=self.checkout, + rooms_available = self.env["pms.room.type.availability"].rooms_available( + checkin=self.checkin, + checkout=self.checkout, room_type_id=self.room_type_id.id or False, ) if rooms_available: @@ -1035,8 +974,6 @@ class PmsReservation(models.Model): "splitted": self.splitted, "room_type_id": self.room_type_id.id, "room_id": self.room_id.id, - "real_checkin": self.real_checkin, - "real_checkout": self.real_checkout, } def confirm(self): @@ -1119,7 +1056,7 @@ class PmsReservation(models.Model): tz_property = self.env.user.pms_property_id.tz today = fields.Date.context_today(self.with_context(tz=tz_property)) days_diff = ( - fields.Date.from_string(self.real_checkin) + fields.Date.from_string(self.checkin) - fields.Date.from_string(today) ).days if days_diff < 0: @@ -1149,29 +1086,6 @@ class PmsReservation(models.Model): ) splitted_reservs.draft() - @api.model - def get_reservations(self, dfrom, dto): - """ - @param dfrom: range date from - @param dto: range date to (NO CHECKOUT, only night) - @return: array with the reservations _confirmed_ between both - dates `dfrom` and `dto` - """ - domain = self._get_domain_reservations_occupation(dfrom, dto) - # _logger.info(domain) - return self.env["pms.reservation"].search(domain) - - @api.model - def _get_domain_reservations_occupation(self, dfrom, dto): - domain = [ - ("reservation_line_ids.date", ">=", dfrom), - ("reservation_line_ids.date", "<=", dto), - ("state", "!=", "cancelled"), - ("overbooking", "=", False), - ("reselling", "=", False), - ] - return domain - # INFO: This function is not in use and should include `dto` in the search @api.model def get_reservations_dates(self, dfrom, dto, room_type=False): @@ -1252,183 +1166,10 @@ class PmsReservation(models.Model): action["target"] = "new" return action - def split(self, nights): - for record in self: - date_start_dt = fields.Date.from_string(record.checkin) - date_end_dt = fields.Date.from_string(record.checkout) - date_diff = abs((date_end_dt - date_start_dt).days) - new_start_date_dt = date_start_dt + timedelta(days=date_diff - nights) - if nights >= date_diff or nights < 1: - raise ValidationError( - _( - "Invalid Nights! Max is \ - '%d'" - ) - % (date_diff - 1) - ) - - vals = record.generate_copy_values( - new_start_date_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), - date_end_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), - ) - # Days Price - reservation_lines = [[], []] - for rline in record.reservation_line_ids: - rline_dt = fields.Date.from_string(rline.date) - if rline_dt >= new_start_date_dt: - reservation_lines[1].append( - ( - 0, - False, - { - "date": rline.date, - "price": rline.price, - "cancel_discount": rline.cancel_discount, - "discount": rline.discount, - "move_line_ids": rline.move_line_ids, - "state": rline.state, - }, - ) - ) - reservation_lines[0].append((2, rline.id, False)) - parent_res = record.parent_reservation or record - vals.update( - { - "splitted": True, - "parent_reservation": parent_res.id, - "room_type_id": parent_res.room_type_id.id, - "state": parent_res.state, - "reservation_line_ids": reservation_lines[1], - "preconfirm": False, - } - ) - reservation_copy = ( - self.env["pms.reservation"] - .with_context({"ignore_avail_restrictions": True}) - .create(vals) - ) - if not reservation_copy: - raise ValidationError( - _( - "Unexpected error copying record. \ - Can't split reservation!" - ) - ) - record.write( - { - "checkout": new_start_date_dt.strftime( - DEFAULT_SERVER_DATETIME_FORMAT - ), - "splitted": True, - "reservation_line_ids": reservation_lines[0], - } - ) - return True - def unify(self): - self.ensure_one() - if not self.splitted: - raise ValidationError(_("This reservation can't be unified")) - - master_reservation = self.parent_reservation or self - - splitted_reservs = self.env["pms.reservation"].search( - [ - ("splitted", "=", True), - ("folio_id", "=", self.folio_id.id), - "|", - ("parent_reservation", "=", master_reservation.id), - ("id", "=", master_reservation.id), - ] - ) - self.unify_books(splitted_reservs) - - self_is_master = master_reservation == self - if not self_is_master: - return {"type": "ir.actions.act_window_close"} - - @api.model - def unify_ids(self, reserv_ids): - splitted_reservs = self.env[self._name].browse(reserv_ids) - self.unify_books(splitted_reservs) - - @api.model - def unify_books(self, splitted_reservs): - parent_reservation = ( - splitted_reservs[0].parent_reservation or splitted_reservs[0] - ) - room_type_ids = splitted_reservs.mapped("room_type_id.id") - if len(room_type_ids) > 1 or ( - len(room_type_ids) == 1 - and parent_reservation.room_type_id.id != room_type_ids[0] - ): - raise ValidationError( - _( - "This reservation can't be unified: They \ - all need to be in the same nº room and room type" - ) - ) - - # Search checkout - last_checkout = splitted_reservs[0].checkout - first_checkin = splitted_reservs[0].checkin - master_reservation = splitted_reservs[0] - for reserv in splitted_reservs: - if last_checkout < reserv.checkout: - last_checkout = reserv.checkout - if first_checkin > reserv.checkin: - first_checkin = reserv.checkin - master_reservation = reserv - - # Agrupate reservation lines - reservation_line_ids = splitted_reservs.mapped("reservation_line_ids") - reservation_line_ids.sorted(key=lambda r: r.date) - rlines = [(5, False, False)] - for rline in reservation_line_ids: - rlines.append( - ( - 0, - False, - { - "date": rline.date, - "price": rline.price, - "cancel_discount": rline.cancel_discount, - "discount": rline.discount, - "move_line_ids": rline.move_line_ids, - "state": rline.state, - }, - ) - ) - - # Unify - osplitted_reservs = splitted_reservs - master_reservation - osplitted_reservs.sudo().unlink() - - _logger.info("========== UNIFY") - _logger.info(master_reservation.real_checkin) - _logger.info(first_checkin) - _logger.info(master_reservation.real_checkout) - _logger.info(last_checkout) - - master_reservation.write( - { - "checkout": last_checkout, - "splitted": master_reservation.real_checkin != first_checkin - or master_reservation.real_checkout != last_checkout, - "reservation_line_ids": rlines, - } - ) + #TODO return True - def open_master(self): - self.ensure_one() - if not self.parent_reservation: - raise ValidationError(_("This is the parent reservation")) - action = self.env.ref("pms.open_pms_reservation_form_tree_all").read()[0] - action["views"] = [(self.env.ref("pms.pms_reservation_view_form").id, "form")] - action["res_id"] = self.parent_reservation.id - return action - def send_reservation_mail(self): return self.folio_id.send_reservation_mail() diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index 06677b48c..cc4c3f593 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -30,6 +30,15 @@ class PmsReservationLine(models.Model): required=True, copy=False, ) + room_id = fields.Many2one( + "pms.room", + string="Room", + ondelete="restrict", + compute="_compute_room_id", + store=True, + readonly=False, + domain="[('id', 'in', reservation_id.allowed_room_ids)]", + ) move_line_ids = fields.Many2many( "account.move.line", "reservation_line_move_rel", @@ -63,8 +72,27 @@ class PmsReservationLine(models.Model): readonly=False, ) discount = fields.Float(string="Discount (%)", digits=("Discount"), default=0.0) + occupies_availability = fields.Boolean( + string = "Occupies", + compute="_compute_occupies_availability", + store=True, + help="This record is taken into account to calculate availability") # Compute and Search methods + @api.depends( + "reservation_id.adults", + "reservation_id.room_type_id", + "reservation_id.room_id") + def _compute_room_id(self): + lines_no_room = self.filtered_domain([("room_id", "=", False)]) + lines_with_room = self - lines_no_room + for line in lines_no_room: + if line.reservation_id.room_id: + line.room_id = line.reservation_id.room_id + # TODO: check_split reservation + # TODO: Allow with confirmation message to + # change de room if the user change the room_type? + @api.depends( "reservation_id", "reservation_id.pricelist_id", @@ -99,6 +127,15 @@ class PmsReservationLine(models.Model): else: line.price = line._origin.price + @api.depends("reservation_id.state", "reservation_id.overbooking") + def _compute_occupies_availability(self): + for line in self: + if line.reservation_id.state == "cancelled" or \ + line.reservation_id.overbooking == True: + line.occupies_availability = False + else: + line.occupies_availability = True + def _recompute_price(self): #REVIEW: Conditional to avoid overriding already calculated prices, # I'm not sure it's the best way @@ -131,10 +168,10 @@ class PmsReservationLine(models.Model): # and pricelist.cancelation_rule_id # ): # date_start_dt = fields.Date.from_string( - # reservation.real_checkin or reservation.checkin + # reservation.checkin # ) # date_end_dt = fields.Date.from_string( - # reservation.real_checkout or reservation.checkout + # reservation.checkout # ) # days = abs((date_end_dt - date_start_dt).days) # rule = pricelist.cancelation_rule_id @@ -153,7 +190,7 @@ class PmsReservationLine(models.Model): # elif reservation.cancelled_reason == "intime": # discount = 100 - # checkin = reservation.real_checkin or reservation.checkin + # checkin = reservation.checkin # dates = [] # for i in range(0, days): # dates.append( diff --git a/pms/models/pms_room_type.py b/pms/models/pms_room_type.py index 0a3dfeb35..946b47611 100644 --- a/pms/models/pms_room_type.py +++ b/pms/models/pms_room_type.py @@ -57,6 +57,14 @@ class PmsRoomType(models.Model): total_rooms_count = fields.Integer(compute="_compute_total_rooms", store=True) active = fields.Boolean("Active", default=True) sequence = fields.Integer("Sequence", default=0) + default_max_avail = fields.Integer("Max. Availability", default=-1, + help="Maximum simultaneous availability on own Booking Engine " + "given no availability rules. " + "Use `-1` for using maximum simultaneous availability.") + default_quota = fields.Integer("Default Quota", default=-1, + help="Quota assigned to the own Booking Engine given no availability rules. " + "Use `-1` for managing no quota.") + _sql_constraints = [ ( @@ -93,25 +101,6 @@ class PmsRoomType(models.Model): capacities = self.room_ids.mapped("capacity") return min(capacities) if any(capacities) else 0 - # TODO: Change name method by rooms_available() - @api.model - def check_availability_room_type(self, dfrom, dto, room_type_id=False, notthis=[]): - """ - Check the max availability for an specific - type of room in a range of dates - """ - reservations = self.env["pms.reservation"].get_reservations(dfrom, dto - timedelta(1)) - reservations_rooms = reservations.mapped("room_id.id") - free_rooms = self.env["pms.room"].search( - [("id", "not in", reservations_rooms), ("id", "not in", notthis)] - ) # TODO: Review if with the new caché V13 We need notthis []¿? - if room_type_id: - rooms_linked = ( - self.env["pms.room.type"].search([("id", "=", room_type_id)]).room_ids - ) - free_rooms = free_rooms & rooms_linked - return free_rooms.sorted(key=lambda r: r.sequence) - @api.model def get_rate_room_types(self, **kwargs): """ diff --git a/pms/models/pms_room_type_availability.py b/pms/models/pms_room_type_availability.py new file mode 100644 index 000000000..fe6b58130 --- /dev/null +++ b/pms/models/pms_room_type_availability.py @@ -0,0 +1,77 @@ +# Copyright 2017 Alexandre Díaz +# Copyright 2017 Dario Lodeiros +# Copyright 2018 Pablo Quesada +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from datetime import timedelta + + + +class PmsRoomTypeAvailability(models.Model): + _name = "pms.room.type.availability" + _description = "Availability" + _inherit = 'mail.thread' + + @api.model + def _default_max_avail(self): + return self.room_type_id.default_max_avail + + @api.model + def _default_quota(self): + return self.room_type_id.default_quota + + # Fields declaration + room_type_id = fields.Many2one('hotel.room.type', 'Room Type', + required=True, + ondelete='cascade') + date = fields.Date('Date', required=True, track_visibility='always') + quota = fields.Integer("Quota", default=_default_quota, + track_visibility='always', + help="Generic Quota assigned.") + max_avail = fields.Integer("Max. Availability", default=-1, readonly=True, + track_visibility='always', + help="Maximum simultaneous availability on own Booking Engine.") + no_web = fields.Boolean('No Web', default=False, + track_visibility='onchange', + help="Set zero availability to the own Booking Engine " + "even when the availability is positive,") + + _sql_constraints = [ + ( + "unique_availability_room_type_rule_date", + "unique(room_type_id, date)", + "The availability rule for this date in this room type already exists, " + "modify it instead of trying to create a new one", + ), + ] + + # Business Methods + @api.model + def rooms_available(self, checkin, checkout, room_type_id=False, current_lines=False): + domain = self._get_domain_reservations_occupation( + dfrom=checkin, + dto=checkout - timedelta(1), + current_lines=current_lines, + ) + reservation_lines = self.env['pms.reservation.line'].search(domain) + reservations_rooms = reservation_lines.mapped("room_id.id") + free_rooms = self.env["pms.room"].search( + [("id", "not in", reservations_rooms)] + ) + if room_type_id: + rooms_linked = ( + self.env["pms.room.type"].search([("id", "=", room_type_id)]).room_ids + ) + free_rooms = free_rooms & rooms_linked + return free_rooms.sorted(key=lambda r: r.sequence) + + @api.model + def _get_domain_reservations_occupation(self, dfrom, dto, current_lines=False): + domain = [ + ("date", ">=", dfrom), + ("date", "<=", dto), + ("occupies_availability", "=", True), + ("id","not in", current_lines), + ] + return domain diff --git a/pms/security/ir.model.access.csv b/pms/security/ir.model.access.csv index 7962107df..d3ac26e76 100644 --- a/pms/security/ir.model.access.csv +++ b/pms/security/ir.model.access.csv @@ -22,8 +22,8 @@ user_access_pms_board_service_line,user_access_pms_board_service_line,model_pms_ user_access_account_partial_reconcile,user_access_account_partial_reconcile,account.model_account_partial_reconcile,pms.group_pms_user,1,1,1,1 user_access_pms_cancelation_rule,user_access_pms_cancelation_rule,model_pms_cancelation_rule,pms.group_pms_user,1,0,0,0 user_access_account_full_reconcile,user_access_account_full_reconcile,account.model_account_full_reconcile,pms.group_pms_user,1,1,1,1 -call_access_pms_cancelation_rule,call_access_pms_cancelation_rule,model_pms_cancelation_rule,base.group_user,1,0,0,0 user_access_property,user_access_property,model_pms_property,pms.group_pms_user,1,0,0,0 +user_access_availability,user_access_availability,model_pms_room_type_availability,pms.group_pms_user,1,0,0,0 manager_access_pms_floor,manager_access_pms_floor,model_pms_floor,pms.group_pms_manager,1,1,1,1 manager_access_pms_amenity,manager_access_pms_amenity,model_pms_amenity,pms.group_pms_manager,1,1,1,1 manager_access_pms_amenity_type,manager_access_pms_amenity_type,model_pms_amenity_type,pms.group_pms_manager,1,1,1,1 @@ -45,25 +45,5 @@ manager_access_pms_board_service_room_type,manager_access_pms_board_service_room manager_access_pms_board_service_room_type_line,manager_access_pms_board_service_room_type_line,model_pms_board_service_room_type_line,pms.group_pms_manager,1,1,1,1 manager_access_pms_board_service_line,manager_access_pms_board_service_line,model_pms_board_service_line,pms.group_pms_manager,1,1,1,1 manager_access_property,manager_access_property,model_pms_property,pms.group_pms_manager,1,1,1,1 -manager_access_pms_cancelation_rule,manager_access_pms_cancelation_rule,model_pms_cancelation_rule,base.group_user,1,1,1,1 -call_access_pms_floor,call_access_pms_floor,model_pms_floor,pms.group_pms_call,1,0,0,0 -call_access_pms_amenity,call_access_pms_amenity,model_pms_amenity,pms.group_pms_call,1,0,0,0 -call_access_pms_amenity_type,call_access_pms_amenity_type,model_pms_amenity_type,pms.group_pms_call,1,0,0,0 -call_access_pms_service,call_access_pms_service,model_pms_service,pms.group_pms_call,1,1,1,1 -call_access_pms_room_type_restriction,call_access_pms_room_type_restriction,model_pms_room_type_restriction,pms.group_pms_call,1,0,0,0 -call_access_pms_reservation_line,call_access_pms_reservation_line,model_pms_reservation_line,pms.group_pms_call,1,1,1,1 -call_access_room_closure_reason,call_access_room_closure_reason,model_room_closure_reason,pms.group_pms_call,1,0,0,0 -call_access_pms_service_line,call_access_pms_service_line,model_pms_service_line,pms.group_pms_call,1,1,1,1 -call_access_pms_board_service,call_access_pms_board_service,model_pms_board_service,pms.group_pms_call,1,0,0,0 -call_access_pms_checkin_partner,call_access_pms_checkin_partner,model_pms_checkin_partner,pms.group_pms_call,1,1,1,1 -call_access_pms_room_type_class,call_access_pms_room_type_class,model_pms_room_type_class,pms.group_pms_call,1,0,0,0 -call_access_pms_room,call_access_pms_room,model_pms_room,pms.group_pms_call,1,0,0,0 -call_access_pms_shared_room,call_access_pms_shared_room,model_pms_shared_room,pms.group_pms_call,1,0,0,0 -call_access_pms_room_type_restriction_item,call_access_pms_room_type_restriction_item,model_pms_room_type_restriction_item,pms.group_pms_call,1,0,0,0 -call_access_pms_reservation,call_access_pms_reservation,model_pms_reservation,pms.group_pms_call,1,1,1,1 -call_access_pms_folio,call_access_pms_folio,model_pms_folio,pms.group_pms_call,1,1,1,1 -call_access_pms_room_type,call_access_pms_room_type,model_pms_room_type,pms.group_pms_call,1,0,0,0 -call_access_pms_board_service_room_type,call_access_pms_board_service_room_type,model_pms_board_service_room_type,pms.group_pms_call,1,0,0,0 -call_access_pms_board_service_room_type_line,call_access_pms_board_service_room_type_line,model_pms_board_service_room_type_line,pms.group_pms_call,1,0,0,0 -call_access_pms_board_service_line,call_access_pms_board_service_line,model_pms_board_service_line,pms.group_pms_call,1,0,0,0 -call_access_property,call_access_property,model_pms_property,pms.group_pms_call,1,0,0,0 +manager_access_pms_cancelation_rule,manager_access_pms_cancelation_rule,model_pms_cancelation_rule,pms.group_pms_manager,1,1,1,1 +user_access_availability,user_access_availability,model_pms_room_type_availability,pms.group_pms_manager,1,1,1,1 diff --git a/pms/templates/pms_email_template.xml b/pms/templates/pms_email_template.xml index d94e4050e..d543cf450 100644 --- a/pms/templates/pms_email_template.xml +++ b/pms/templates/pms_email_template.xml @@ -53,7 +53,7 @@ - + diff --git a/pms/wizard/wizard_reservation.py b/pms/wizard/wizard_reservation.py index 6c21549d6..db2d19bea 100644 --- a/pms/wizard/wizard_reservation.py +++ b/pms/wizard/wizard_reservation.py @@ -380,7 +380,7 @@ class PmsRoomTypeWizards(models.TransientModel): [("room_type_id", "=", res.room_type_id.id)] ) real_max = len( - res.room_type_id.check_availability_room_type( + self.env["pms.room.type.availability"].rooms_available( res.checkin, ( fields.Date.from_string(res.checkout) - timedelta(days=1)