# Copyright 2018 Alexandre Díaz # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging from datetime import timedelta from odoo import models, fields, api, _ from odoo.exceptions import ValidationError from odoo.tools import DEFAULT_SERVER_DATE_FORMAT _logger = logging.getLogger(__name__) class HotelReservation(models.Model): _inherit = 'hotel.reservation' reserve_color = fields.Char(compute='_compute_color', string='Color', store=True) reserve_color_text = fields.Char(compute='_compute_color', string='Color', store=True) @api.multi def _generate_color(self): self.ensure_one() reserv_color = '#FFFFFF' reserv_color_text = '#000000' user = self.env.user if self.reservation_type == 'staff': reserv_color = user.color_staff reserv_color_text = user.color_letter_staff elif self.reservation_type == 'out': reserv_color = user.color_dontsell reserv_color_text = user.color_letter_dontsell elif self.to_assign: reserv_color = user.color_to_assign reserv_color_text = user.color_letter_to_assign elif self.state == 'draft': reserv_color = user.color_pre_reservation reserv_color_text = user.color_letter_pre_reservation elif self.state == 'confirm': if self.folio_id.pending_amount <= 0: reserv_color = user.color_reservation_pay reserv_color_text = user.color_letter_reservation_pay else: reserv_color = user.color_reservation reserv_color_text = user.color_letter_reservation elif self.state == 'booking': if self.folio_id.pending_amount <= 0: reserv_color = user.color_stay_pay reserv_color_text = user.color_letter_stay_pay else: reserv_color = user.color_stay reserv_color_text = user.color_letter_stay else: if self.folio_id.pending_amount <= 0: reserv_color = user.color_checkout reserv_color_text = user.color_letter_checkout else: reserv_color = user.color_payment_pending reserv_color_text = user.color_letter_payment_pending return (reserv_color, reserv_color_text) @api.depends('state', 'reservation_type', 'folio_id.pending_amount', 'to_assign') def _compute_color(self): for record in self: colors = record._generate_color() record.update({ 'reserve_color': colors[0], 'reserve_color_text': colors[1], }) @api.model def _hcalendar_reservation_data(self, reservations): json_reservations = [] json_reservation_tooltips = {} for reserv in reservations: json_reservations.append({ 'room_id': reserv['room_id'], 'id': reserv['id'], 'name': reserv['closure_reason'] or _('Out of service') if reserv['reservation_type'] == 'out' else reserv['partner_name'], 'adults': reserv['adults'], 'childrens': reserv['children'], 'checkin': reserv['checkin'], 'checkout': reserv['checkout'], 'folio_id': reserv['folio_id'], 'bgcolor': reserv['reserve_color'], 'color': reserv['reserve_color_text'], 'splitted': reserv['splitted'], 'parent_reservation': reserv['parent_reservation'] or False, 'read_only': False, # Read-Only 'fix_days': reserv['splitted'], # Fix Days 'fix_room': False, # Fix Rooms 'overbooking': reserv['overbooking'], 'state': reserv['state'], 'real_dates': [reserv['real_checkin'], reserv['real_checkout']]}) json_reservation_tooltips.update({ reserv['id']: { 'folio_name': reserv['folio_id'], 'name': _('Out of service') if reserv['reservation_type'] == 'out' else reserv['partner_name'], 'phone': reserv['mobile'] or reserv['phone'] or _('Phone not provided'), 'email': reserv['email'] or _('Email not provided'), 'room_type_name': reserv['room_type'], 'adults': reserv['adults'], 'children': reserv['children'], 'checkin': reserv['checkin'], 'checkout': reserv['checkout'], 'arrival_hour': reserv['arrival_hour'], 'departure_hour': reserv['departure_hour'], 'amount_total': reserv['amount_total'], 'pending_amount': reserv['pending_amount'], 'amount_paid': reserv['amount_total'] - (reserv['pending_amount'] or 0.0), 'type': reserv['reservation_type'] or 'normal', 'closure_reason': reserv['closure_reason'], 'out_service_description': reserv['out_service_description'] or _('No reason given'), 'splitted': reserv['splitted'], 'channel_type': reserv['channel_type'], 'real_dates': [reserv['real_checkin'], reserv['real_checkout']], # TODO: Add Board Services and Extra Service as Cradle, Bed, ... 'board_service_name': reserv['board_service_name'], 'services': reserv['services'], } }) return (json_reservations, json_reservation_tooltips) @api.model def _hcalendar_room_data(self, rooms): pricelist_id = self.env['ir.default'].sudo().get( 'res.config.settings', 'default_pricelist_id') if pricelist_id: pricelist_id = int(pricelist_id) json_rooms = [ { 'id': room.id, 'name': room.name, 'capacity': room.capacity, 'class_name': room.room_type_id.class_id.name, 'class_id': room.room_type_id.class_id.id, 'shared': room.shared_room, 'price': room.room_type_id and ['pricelist', room.room_type_id.id, pricelist_id, room.room_type_id.name] or 0, 'room_type_name': room.room_type_id.name, 'room_type_id': room.room_type_id.id, 'floor_id': room.floor_id.id, 'amentity_ids': room.room_type_id.room_amenity_ids.ids, } for room in rooms] return json_rooms @api.model def _hcalendar_calendar_data(self, calendars): return [ { 'id': calendar.id, 'name': calendar.name, 'segmentation_ids': calendar.segmentation_ids.ids, 'location_ids': calendar.location_ids.ids, 'amenity_ids': calendar.amenity_ids.ids, 'room_type_ids': calendar.room_type_ids.ids, } for calendar in calendars] @api.model def _hcalendar_event_data(self, events): json_events = [ { 'id': event.id, 'name': event.name, 'date': event.start, 'location': event.location, } for event in events] return json_events @api.model def get_hcalendar_calendar_data(self): calendars = self.env['hotel.calendar'].search([]) res = self._hcalendar_calendar_data(calendars) return res @api.model def get_hcalendar_reservations_data(self, dfrom_dt, dto_dt, rooms): rdfrom_dt = dfrom_dt + timedelta(days=1) # Ignore checkout rdfrom_str = rdfrom_dt.strftime(DEFAULT_SERVER_DATE_FORMAT) dto_str = dto_dt.strftime(DEFAULT_SERVER_DATE_FORMAT) self.env.cr.execute(''' SELECT hr.id, hr.room_id, hr.adults, hr.children, hr.checkin, hr.checkout, hr.reserve_color, hr.reserve_color_text, hr.splitted, hr.parent_reservation, hr.overbooking, hr.state, hr.real_checkin, hr.real_checkout, hr.out_service_description, hr.arrival_hour, hr.departure_hour, hr.channel_type, hf.id as folio_id, hf.name as folio_name, hf.reservation_type, hf.amount_total, hf.pending_amount, rp.mobile, rp.phone, rp.email, rp.name as partner_name, pt.name as room_type, array_agg(pt2.name) FILTER (WHERE pt2.is_popoverable = TRUE) as services, rcr.name as closure_reason, hbs.name as board_service_name FROM hotel_reservation AS hr LEFT JOIN hotel_folio AS hf ON hr.folio_id = hf.id LEFT JOIN hotel_room_type AS hrt ON hr.room_type_id = hrt.id LEFT JOIN product_product AS pp ON hrt.product_id = pp.id LEFT JOIN product_template AS pt ON pp.product_tmpl_id = pt.id LEFT JOIN res_partner AS rp ON hf.partner_id = rp.id LEFT JOIN room_closure_reason as rcr ON hf.closure_reason_id = rcr.id LEFT JOIN hotel_board_service_room_type_rel AS hbsrt ON hr.board_service_room_id = hbsrt.id LEFT JOIN hotel_board_service AS hbs ON hbsrt.hotel_board_service_id = hbs.id LEFT JOIN hotel_service AS hs ON hr.id = hs.ser_room_line LEFT JOIN product_product AS pp2 ON hs.product_id = pp2.id LEFT JOIN product_template AS pt2 ON pp2.product_tmpl_id = pt2.id WHERE room_id IN %s AND ( (checkin <= %s AND checkout >= %s AND checkout <= %s) OR (checkin >= %s AND checkout <= %s) OR (checkin >= %s AND checkin <= %s AND checkout >= %s) OR (checkin <= %s AND checkout >= %s)) GROUP BY hr.id, hf.id, pt.name, rcr.name, hbs.name, rp.mobile, rp.phone, rp.email, rp.name ORDER BY checkin DESC, checkout ASC, adults DESC, children DESC ''', (tuple(rooms.ids), rdfrom_str, rdfrom_str, dto_str, rdfrom_str, dto_str, rdfrom_str, dto_str, dto_str, rdfrom_str, dto_str)) return self._hcalendar_reservation_data(self.env.cr.dictfetchall()) @api.model def get_hcalendar_pricelist_data(self, dfrom_dt, dto_dt): pricelist_id = self.env['ir.default'].sudo().get( 'res.config.settings', 'default_pricelist_id') if pricelist_id: pricelist_id = int(pricelist_id) room_types_ids = self.env['hotel.room.type'].search([]) dfrom_str = dfrom_dt.strftime(DEFAULT_SERVER_DATE_FORMAT) dto_str = dto_dt.strftime(DEFAULT_SERVER_DATE_FORMAT) self.env.cr.execute(''' WITH RECURSIVE gen_table_days AS ( SELECT hrt.id, %s::Date AS date FROM hotel_room_type AS hrt UNION ALL SELECT hrt.id, (td.date + INTERVAL '1 day')::Date FROM gen_table_days as td LEFT JOIN hotel_room_type AS hrt ON hrt.id=td.id WHERE td.date < %s ) SELECT TO_CHAR(gtd.date, 'DD/MM/YYYY') as date, gtd.id as room_type_id, pt.name, ppi.fixed_price as price, pt.list_price FROM gen_table_days AS gtd LEFT JOIN hotel_room_type AS hrt ON hrt.id = gtd.id LEFT JOIN product_product AS pp ON pp.id = hrt.product_id LEFT JOIN product_template AS pt ON pt.id = pp.product_tmpl_id LEFT JOIN product_pricelist_item AS ppi ON ppi.date_start = gtd.date AND ppi.date_end = gtd.date AND ppi.product_tmpl_id = pt.id WHERE gtd.id IN %s ORDER BY gtd.id ASC, gtd.date ASC ''', (dfrom_str, dto_str, tuple(room_types_ids.ids))) query_results = self.env.cr.dictfetchall() json_data = {} for results in query_results: if results['room_type_id'] not in json_data: json_data.setdefault(results['room_type_id'], {}).update({ 'title': results['name'], 'room': results['room_type_id'], }) json_data[results['room_type_id']].setdefault('days', {}).update({ results['date']: results['price'] or results['list_price'] }) json_rooms_prices = {} for prices in list(json_data.values()): json_rooms_prices.setdefault(pricelist_id, []).append(prices) return json_rooms_prices @api.model def get_hcalendar_restrictions_data(self, dfrom_dt, dto_dt): restriction_id = self.env['ir.default'].sudo().get( 'res.config.settings', 'default_restriction_id') if restriction_id: restriction_id = int(restriction_id) # Get Restrictions json_rooms_rests = {} room_typed_ids = self.env['hotel.room.type'].search( [], order='hcal_sequence ASC') room_type_rest_obj = self.env['hotel.room.type.restriction.item'] rtype_rest_ids = room_type_rest_obj.search([ ('room_type_id', 'in', room_typed_ids.ids), ('date', '>=', dfrom_dt), ('date', '<=', dto_dt), ('restriction_id', '=', restriction_id) ]) for room_type in room_typed_ids: days = {} rest_ids = rtype_rest_ids.filtered( lambda x: x.room_type_id == room_type) for rest_id in rest_ids: days.update({ fields.Date.from_string(rest_id.date).strftime("%d/%m/%Y"): ( rest_id.min_stay, rest_id.min_stay_arrival, rest_id.max_stay, rest_id.max_stay_arrival, rest_id.closed, rest_id.closed_arrival, rest_id.closed_departure) }) json_rooms_rests.update({room_type.id: days}) return json_rooms_rests @api.model def get_hcalendar_events_data(self, dfrom_dt, dto_dt): user_id = self.env['res.users'].browse(self.env.uid) domain = [ '|', '&', ('start', '<=', dto_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)), ('stop', '>=', dfrom_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)), '&', ('start', '>=', dfrom_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)), ('stop', '<=', dto_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)) ] if user_id.pms_allowed_events_tags: domain.append(('categ_ids', 'in', user_id.pms_allowed_events_tags)) if user_id.pms_denied_events_tags: domain.append( ('categ_ids', 'not in', user_id.pms_denied_events_tags)) events_raw = self.env['calendar.event'].search(domain) return self._hcalendar_event_data(events_raw) @api.model def get_hcalendar_settings(self): user_id = self.env['res.users'].browse(self.env.uid) type_move = user_id.pms_type_move return { 'divide_rooms_by_capacity': user_id.pms_divide_rooms_by_capacity, 'eday_week': user_id.pms_end_day_week, 'eday_week_offset': user_id.pms_end_day_week_offset, 'days': user_id.pms_default_num_days, 'allow_invalid_actions': type_move == 'allow_invalid', 'assisted_movement': type_move == 'assisted', 'default_arrival_hour': self.env['ir.default'].sudo().get( 'res.config.settings', 'default_arrival_hour'), 'default_departure_hour': self.env['ir.default'].sudo().get( 'res.config.settings', 'default_departure_hour'), 'show_notifications': user_id.pms_show_notifications, 'show_pricelist': user_id.pms_show_pricelist, 'show_availability': user_id.pms_show_availability, 'show_num_rooms': user_id.pms_show_num_rooms, } @api.model def get_hcalendar_all_data(self, dfrom, dto, withRooms=True): if not dfrom or not dto: raise ValidationError(_('Input Error: No dates defined!')) dfrom_dt = fields.Date.from_string(dfrom) dto_dt = fields.Date.from_string(dto) rooms = self.env['hotel.room'].search([], order='hcal_sequence ASC') json_res, json_res_tooltips = self.get_hcalendar_reservations_data( dfrom_dt, dto_dt, rooms) vals = { 'rooms': withRooms and self._hcalendar_room_data(rooms) or [], 'reservations': json_res, 'tooltips': json_res_tooltips, 'pricelist': self.get_hcalendar_pricelist_data(dfrom_dt, dto_dt), 'restrictions': self.get_hcalendar_restrictions_data(dfrom_dt, dto_dt), 'events': self.get_hcalendar_events_data(dfrom_dt, dto_dt), 'calendars': withRooms and self.get_hcalendar_calendar_data() or [] } return vals @api.multi def generate_bus_values(self, naction, ntype, ntitle=''): self.ensure_one() return { 'action': naction, 'type': ntype, 'title': ntitle, 'room_id': self.room_id.id, 'reserv_id': self.id, 'partner_name': (self.closure_reason_id.name or _('Out of service')) if self.reservation_type == 'out' else self.partner_id.name, 'adults': self.adults, 'children': self.children, 'checkin': self.checkin, 'checkout': self.checkout, 'arrival_hour': self.arrival_hour, 'departure_hour': self.departure_hour, 'folio_id': self.folio_id.id, 'reserve_color': self.reserve_color, 'reserve_color_text': self.reserve_color_text, 'splitted': self.splitted, 'parent_reservation': self.parent_reservation and self.parent_reservation.id or 0, 'room_name': self.room_id.name, 'room_type_name': self.room_type_id.name, 'partner_phone': self.partner_id.mobile or self.partner_id.phone or _('Undefined'), 'partner_email': self.partner_id.email or _('Undefined'), 'state': self.state, 'fix_days': self.splitted, 'overbooking': self.overbooking, 'amount_total': self.folio_id.amount_total, 'pending_amount': self.folio_id.pending_amount, 'amount_paid': self.folio_id.amount_total - self.folio_id.pending_amount, 'reservation_type': self.reservation_type or 'normal', 'closure_reason': self.closure_reason_id.name, 'out_service_description': self.out_service_description or _('No reason given'), 'real_dates': [self.real_checkin, self.real_checkout], 'channel_type': self.channel_type, 'board_service_name': self.board_service_room_id.hotel_board_service_id.name, 'services': [service.product_id.name for service in self.service_ids if service.product_id.is_popoverable] or False, } @api.multi def send_bus_notification(self, naction, ntype, ntitle=''): hotel_cal_obj = self.env['bus.hotel.calendar'] for record in self: hotel_cal_obj.send_reservation_notification( record.generate_bus_values(naction, ntype, ntitle)) @api.model def swap_reservations(self, fromReservsIds, toReservsIds): from_reservs = self.env['hotel.reservation'].browse(fromReservsIds) to_reservs = self.env['hotel.reservation'].browse(toReservsIds) if not any(from_reservs) or not any(to_reservs): raise ValidationError(_("Invalid swap parameters")) max_from_persons = max( from_reservs.mapped(lambda x: x.adults)) max_to_persons = max( to_reservs.mapped(lambda x: x.adults)) from_room = from_reservs[0].room_id to_room = to_reservs[0].room_id from_overbooking = from_reservs[0].overbooking to_overbooking = to_reservs[0].overbooking if max_from_persons > to_room.capacity or \ max_to_persons > from_room.capacity: raise ValidationError("Invalid swap operation: wrong capacity") for record in from_reservs: record.with_context({'ignore_avail_restrictions': True}).write({ 'room_id': to_room.id, 'overbooking': to_overbooking, }) for record in to_reservs: record.with_context({'ignore_avail_restrictions': True}).write({ 'room_id': from_room.id, 'overbooking': from_overbooking, }) return True @api.model def create(self, vals): reservation_id = super(HotelReservation, self).create(vals) reservation_id.send_bus_notification('create', 'notify', _("Reservation Created")) return reservation_id @api.multi def write(self, vals): _logger.info("RESERV WRITE") ret = super(HotelReservation, self).write(vals) if 'partner_id' in vals or 'checkin' in vals or \ 'checkout' in vals or 'product_id' in vals or \ 'adults' in vals or 'children' in vals or \ 'state' in vals or 'splitted' in vals or \ 'closure_reason_id' in vals or 'out_service_description' in vals or \ 'reservation_type' in vals or \ 'price_total' in vals or \ 'parent_reservation' in vals or 'overbooking' in vals or \ 'room_type_id' in vals: for record in self: record.send_bus_notification( 'write', (record.state == 'cancelled') and 'warn' or 'notify', (record.state == 'cancelled') and _("Reservation Cancelled") or _("Reservation Changed") ) elif not any(vals) or 'to_read' in vals or 'to_assign' in vals: self.send_bus_notification('write', 'noshow') return ret @api.multi def unlink(self): self.send_bus_notification('unlink', 'warn', _("Reservation Deleted")) return super(HotelReservation, self).unlink()