# -*- coding: utf-8 -*- # Copyright 2017 Alexandre Díaz # Copyright 2017 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo.exceptions import except_orm, UserError, ValidationError from odoo.tools import ( misc, DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT) from odoo import models, fields, api, _ from decimal import Decimal from dateutil.relativedelta import relativedelta from datetime import datetime, timedelta, date from odoo.addons.hotel import date_utils import pytz import time import logging _logger = logging.getLogger(__name__) from odoo.addons import decimal_precision as dp class HotelReservation(models.Model): @api.multi def _generate_color(self): self.ensure_one() now_utc_dt = date_utils.now() # unused variables # diff_checkin_now = date_utils.date_diff(now_utc_dt, self.checkin, # hours=False) # diff_checkout_now = date_utils.date_diff(now_utc_dt, self.checkout, # hours=False) ir_values_obj = self.env['ir.default'] reserv_color = '#FFFFFF' reserv_color_text = '#000000' # FIXME added for migration return ('#4E9DC4', '#000000') if self.reservation_type == 'staff': reserv_color = ir_values_obj.get('res.config.settings', 'color_staff') reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_staff') elif self.reservation_type == 'out': reserv_color = ir_values_obj.get('res.config.settings', 'color_dontsell') reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_dontsell') elif self.to_assign: reserv_color = ir_values_obj.get('res.config.settings', 'color_to_assign') reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_to_assign') elif self.state == 'draft': reserv_color = ir_values_obj.get('res.config.settings', 'color_pre_reservation') reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_pre_reservation') elif self.state == 'confirm': if self.folio_id.invoices_amount == 0: reserv_color = ir_values_obj.get( 'res.config.settings', 'color_reservation_pay') reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_reservation_pay') else: reserv_color = ir_values_obj.get( 'res.config.settings', 'color_reservation') reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_reservation') elif self.state == 'booking': if self.folio_id.invoices_amount == 0: reserv_color = ir_values_obj.get( 'res.config.settings', 'color_stay_pay') reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_stay_pay') else: reserv_color = ir_values_obj.get( 'res.config.settings', 'color_stay') reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_stay') else: if self.folio_id.invoices_amount == 0: reserv_color = ir_values_obj.get( 'res.config.settings', 'color_checkout') reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_checkout') else: reserv_color = ir_values_obj.get( 'res.config.settings', 'color_payment_pending') reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_payment_pending') return (reserv_color, reserv_color_text) @api.depends('state', 'reservation_type', 'folio_id.invoices_amount', 'to_assign') def _compute_color(self): _logger.info('_compute_color') for rec in self: colors = rec._generate_color() rec.update({ 'reserve_color': colors[0], 'reserve_color_text': colors[1], }) rec.folio_id.color = colors[0] # hotel_reserv_obj = self.env['hotel.reservation'] # if rec.splitted: # master_reservation = rec.parent_reservation or rec # splitted_reservs = hotel_reserv_obj.search([ # ('splitted', '=', True), # '|', ('parent_reservation', '=', master_reservation.id), # ('id', '=', master_reservation.id), # ('folio_id', '=', rec.folio_id.id), # ('id', '!=', rec.id), # ]) # splitted_reservs.write({'reserve_color': rec.reserve_color}) @api.multi def copy(self, default=None): ''' @param self: object pointer @param default: dict of default values to be set ''' return super(HotelReservation, self).copy(default=default) @api.multi def _amount_line(self, field_name, arg): ''' @param self: object pointer @param field_name: Names of fields. @param arg: User defined arguments ''' return False # return self.env['sale.order.line']._amount_line(field_name, arg) @api.multi def _number_packages(self, field_name, arg): ''' @param self: object pointer @param field_name: Names of fields. @param arg: User defined arguments ''' return False # return self.env['sale.order.line']._number_packages(field_name, arg) @api.multi def set_call_center_user(self): user = self.env['res.users'].browse(self.env.uid) rec.call_center = user.has_group('hotel.group_hotel_call') @api.multi def _get_default_checkin(self): folio = False default_arrival_hour = self.env['ir.default'].sudo().get( 'res.config.settings', 'default_arrival_hour') if 'folio_id' in self._context: folio = self.env['hotel.folio'].search([ ('id', '=', self._context['folio_id']) ]) if folio and folio.room_lines: return folio.room_lines[0].checkin else: tz_hotel = self.env['ir.default'].sudo().get( 'res.config.settings', 'tz_hotel') now_utc_dt = date_utils.now() ndate = "%s %s:00" % \ (now_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT), default_arrival_hour) ndate_dt = date_utils.get_datetime(ndate, stz=tz_hotel) ndate_dt = date_utils.dt_as_timezone(ndate_dt, 'UTC') return ndate_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) @api.model def _get_default_checkout(self): folio = False default_departure_hour = self.env['ir.default'].sudo().get( 'res.config.settings', 'default_departure_hour') if 'folio_id' in self._context: folio = self.env['hotel.folio'].search([ ('id', '=', self._context['folio_id']) ]) if folio and folio.room_lines: return folio.room_lines[0].checkout else: tz_hotel = self.env['ir.default'].sudo().get( 'res.config.settings', 'tz_hotel') now_utc_dt = date_utils.now() + timedelta(days=1) ndate = "%s %s:00" % \ (now_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT), default_departure_hour) ndate_dt = date_utils.get_datetime(ndate, stz=tz_hotel) ndate_dt = date_utils.dt_as_timezone(ndate_dt, 'UTC') return ndate_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) # @api.constrains('checkin', 'checkout') #Why dont run api.depends?¿? # def _computed_nights(self): # for res in self: # if res.checkin and res.checkout: # nights = days_diff = date_utils.date_diff( # self.checkin, # self.checkout, hours=False) # res.nights = nights @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): if args is None: args = [] if not(name == '' and operator == 'ilike'): args += [ '|', ('folio_id.name', operator, name) # FIXME Remove product inheritance # ('product_id.name', operator, name) ] return super(HotelReservation, self).name_search( name='', args=args, operator='ilike', limit=limit) @api.multi def name_get(self): # FIXME Remove product inheritance result = [] for res in self: name = u'%s (%s)' % (res.folio_id.name, res.room_id.name) result.append((res.id, name)) return result # FIXME added for migration def _compute_qty_delivered_updateable(self): pass # FIXME added for migration def _compute_invoice_status(self): pass _name = 'hotel.reservation' _description = 'Hotel Reservation' _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] _order = "last_updated_res desc, name" # The record's name should now be used for description of the reservation ? name = fields.Text('Reservation Description', required=True) # _defaults = { # 'product_id': False # } room_id = fields.Many2one('hotel.room', string='Room') reservation_no = fields.Char('Reservation No', size=64, readonly=True) adults = fields.Integer('Adults', size=64, readonly=False, track_visibility='onchange', help='List of adults there in guest list. ') children = fields.Integer('Children', size=64, readonly=False, track_visibility='onchange', help='Number of children there in guest list.') to_assign = fields.Boolean('To Assign', track_visibility='onchange') state = fields.Selection([('draft', 'Pre-reservation'), ('confirm', 'Pending Entry'), ('booking', 'On Board'), ('done', 'Out'), ('cancelled', 'Cancelled')], 'State', readonly=True, default=lambda *a: 'draft', track_visibility='onchange') reservation_type = fields.Selection(related='folio_id.reservation_type', default=lambda *a: 'normal') cancelled_reason = fields.Selection([ ('late', 'Late'), ('intime', 'In time'), ('noshow', 'No Show')], 'Cause of cancelled') out_service_description = fields.Text('Cause of out of service') folio_id = fields.Many2one('hotel.folio', string='Folio', ondelete='cascade') checkin = fields.Datetime('Check In', required=True, default=_get_default_checkin, track_visibility='onchange') checkout = fields.Datetime('Check Out', required=True, default=_get_default_checkout, track_visibility='onchange') room_type_id = fields.Many2one('hotel.room.type', string='Room Type', required=True, track_visibility='onchange') partner_id = fields.Many2one(related='folio_id.partner_id') company_id = fields.Many2one('res.company', 'Company') reservation_line_ids = fields.One2many('hotel.reservation.line', 'reservation_id', readonly=True, required=True, states={ 'draft': [('readonly', False)], 'sent': [('readonly', False)], 'confirm': [('readonly', False)], 'booking': [('readonly', False)], }) reserve_color = fields.Char(compute='_compute_color', string='Color', store=True) reserve_color_text = fields.Char(compute='_compute_color', string='Color', store=True) service_line_ids = fields.One2many('hotel.service', 'ser_room_line') # pricelist_id = fields.Many2one('product.pricelist', # related='folio_id.pricelist_id', # readonly="1") cardex_ids = fields.One2many('cardex', 'reservation_id') # TODO: As cardex_count is a computed field, it can't not be used in a domain filer # Non-stored field hotel.reservation.cardex_count cannot be searched # searching on a computed field can also be enabled by setting the search parameter. # The value is a method name returning a Domains cardex_count = fields.Integer('Cardex counter', compute='_compute_cardex_count') cardex_pending = fields.Boolean('Cardex Pending', compute='_compute_cardex_count', search='_search_cardex_pending') cardex_pending_num = fields.Integer('Cardex Pending Num', compute='_compute_cardex_count') # check_rooms = fields.Boolean('Check Rooms') is_checkin = fields.Boolean() is_checkout = fields.Boolean() splitted = fields.Boolean('Splitted', default=False) parent_reservation = fields.Many2one('hotel.reservation', 'Parent Reservation') overbooking = fields.Boolean('Is Overbooking', default=False) # To show de total amount line in read_only mode amount_reservation = fields.Float('Total', compute='_computed_amount_reservation', store=True) amount_reservation_services = fields.Float('Services Amount', compute='_computed_amount_reservation', store=True) amount_room = fields.Float('Amount Room', compute="_computed_amount_reservation", store=True) amount_discount = fields.Float('Room with Discount', compute="_computed_amount_reservation", store=True) discount_type = fields.Selection([ ('percent', 'Percent'), ('fixed', 'Fixed')], 'Discount Type', default=lambda *a: 'percent') discount_fixed = fields.Float('Fixed Discount') nights = fields.Integer('Nights', compute='_computed_nights', store=True) channel_type = fields.Selection([ ('door', 'Door'), ('mail', 'Mail'), ('phone', 'Phone'), ('call', 'Call Center'), ('web', 'Web')], 'Sales Channel', default='door') last_updated_res = fields.Datetime('Last Updated') # Monetary to Float folio_pending_amount = fields.Float(related='folio_id.invoices_amount') segmentation_ids = fields.Many2many(related='folio_id.segmentation_ids') shared_folio = fields.Boolean(compute='_computed_shared') #Used to notify is the reservation folio has other reservations or services email = fields.Char('E-mail', related='partner_id.email') mobile = fields.Char('Mobile', related='partner_id.mobile') phone = fields.Char('Phone', related='partner_id.phone') partner_internal_comment = fields.Text(string='Internal Partner Notes', related='partner_id.comment') folio_internal_comment = fields.Text(string='Internal Folio Notes', related='folio_id.internal_comment') preconfirm = fields.Boolean('Auto confirm to Save', default=True) call_center = fields.Boolean(compute='set_call_center_user') to_send = fields.Boolean('To Send', default=True) has_confirmed_reservations_to_send = fields.Boolean( related='folio_id.has_confirmed_reservations_to_send', readonly=True) has_cancelled_reservations_to_send = fields.Boolean( related='folio_id.has_cancelled_reservations_to_send', readonly=True) has_checkout_to_send = fields.Boolean( related='folio_id.has_checkout_to_send', readonly=True) # fix_total = fields.Boolean(compute='_compute_fix_total') # fix_folio_pending = fields.Boolean(related='folio_id.fix_price') # order_line = fields.One2many('sale.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True, auto_join=True) # product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product') # product_uom = fields.Many2one('product.uom', string='Unit of Measure', required=True) # product_uom_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1.0) # currency_id = fields.Many2one('res.currency', # related='pricelist_id.currency_id', # string='Currency', readonly=True, required=True) # invoice_status = fields.Selection([ # ('upselling', 'Upselling Opportunity'), # ('invoiced', 'Fully Invoiced'), # ('to invoice', 'To Invoice'), # ('no', 'Nothing to Invoice') # ], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no') tax_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) # qty_to_invoice = fields.Float( # string='To Invoice', store=True, readonly=True, # digits=dp.get_precision('Product Unit of Measure')) # qty_invoiced = fields.Float( # compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True, # digits=dp.get_precision('Product Unit of Measure')) # qty_delivered = fields.Float(string='Delivered', copy=False, digits=dp.get_precision('Product Unit of Measure'), default=0.0) # qty_delivered_updateable = fields.Boolean(compute='_compute_qty_delivered_updateable', string='Can Edit Delivered', readonly=True, default=True) price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'), default=0.0) # Monetary to Float price_subtotal = fields.Float(compute='_compute_amount', string='Subtotal', readonly=True, store=True) # Monetary to Float price_total = fields.Float(compute='_compute_amount', string='Total', readonly=True, store=True) # FIXME discount per night # discount = fields.Float(string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0) # analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') def action_recalcule_payment(self): for record in self: for res in record.folio_id.room_lines: res.on_change_checkin_checkout_product_id() def _computed_folio_name(self): for res in self: res.folio_name = res.folio_id.name + '-' + \ res.folio_id.date_order @api.multi def send_reservation_mail(self): return self.folio_id.send_reservation_mail() @api.multi def send_exit_mail(self): return self.folio_id.send_exit_mail() @api.multi def send_cancel_mail(self): return self.folio_id.send_cancel_mail() @api.multi def action_checks(self): self.ensure_one() return { 'name': _('Cardexs'), 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'cardex', 'type': 'ir.actions.act_window', 'domain': [('reservation_id', '=', self.id)], 'target': 'new', } @api.multi def _computed_shared(self): for record in self: if record.folio_id: if len(record.folio_id.room_lines) > 1 or \ record.folio_id.service_line_ids.filtered(lambda x: ( x.ser_room_line != record.id)): record.shared_folio = True else: record.shared_folio = False @api.depends('checkin', 'checkout') def _computed_nights(self): for res in self: if res.checkin and res.checkout: nights = days_diff = date_utils.date_diff( res.checkin, res.checkout, hours=False) res.nights = nights @api.model def recompute_reservation_totals(self): reservations = self.env['hotel.reservation'].search([]) for res in reservations: if res.folio_id.state not in ('done','cancel'): _logger.info('---------BOOK-----------') _logger.info(res.amount_reservation) _logger.info(res.id) res._computed_amount_reservation() _logger.info(res.amount_reservation) _logger.info('---------------------------') @api.depends('reservation_line_ids.price') def _computed_amount_reservation(self): _logger.info('_computed_amount_reservation') # FIXME commented during migration # import wdb; wdb.set_trace() # for res in self: # amount_service = amount_room = 0 # for line in res.reservation_line_ids: # amount_room += line.price # for service in res.service_line_ids: # # We must calc the line to can show the price in edit mode # # on smartbutton whithout having to wait to save. # total_line = service.price_unit * service.product_uom_qty # discount = (service.discount * total_line) / 100 # amount_service += total_line - discount # res.amount_room = amount_room #To view price_unit with read_only # if res.discount_type == 'fixed' and amount_room > 0: # res.discount = (res.discount_fixed * 100) / amount_room # WARNING Posible division by zero # else: # res.discount_fixed = (res.discount * amount_room) / 100 # res.amount_discount = amount_room - res.discount_fixed # res.price_unit = amount_room # res.amount_reservation_services = amount_service # res.amount_reservation = res.amount_discount + amount_service #To the smartbutton @api.multi def _compute_cardex_count(self): _logger.info('_compute_cardex_count') for res in self: res.cardex_count = len(res.cardex_ids) res.cardex_pending_num = (res.adults + res.children) \ - len(res.cardex_ids) if (res.adults + res.children - len(res.cardex_ids)) <= 0: res.cardex_pending = False else: res.cardex_pending = True # https://www.odoo.com/es_ES/forum/ayuda-1/question/calculated-fields-in-search-filter-possible-118501 @api.multi def _search_cardex_pending(self, operator, value): recs = self.search([]).filtered(lambda x: x.cardex_pending is True) if recs: return [('id', 'in', [x.id for x in recs])] @api.multi def action_pay_folio(self): self.ensure_one() return self.folio_id.action_pay() @api.multi def action_pay_reservation(self): self.ensure_one() partner = self.partner_id.id amount = min(self.amount_reservation, self.folio_pending_amount) note = self.folio_id.name + ' (' + self.name + ')' view_id = self.env.ref('hotel.view_account_payment_folio_form').id return{ 'name': _('Register Payment'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.payment', 'type': 'ir.actions.act_window', 'view_id': view_id, 'context': { 'default_folio_id': self.folio_id.id, 'default_room_id': self.id, 'default_amount': amount, 'default_payment_type': 'inbound', 'default_partner_type': 'customer', 'default_partner_id': partner, 'default_communication': note, }, 'target': 'new', } @api.model def daily_plan(self): _logger.info('daily_plan') today_utc_dt = date_utils.now() yesterday_utc_dt = today_utc_dt - timedelta(days=1) hotel_tz = self.env['ir.default'].sudo().get('res.config.settings', 'tz_hotel') today_dt = date_utils.dt_as_timezone(today_utc_dt, hotel_tz) yesterday_dt = date_utils.dt_as_timezone(yesterday_utc_dt, hotel_tz) today_str = today_dt.strftime(DEFAULT_SERVER_DATE_FORMAT) yesterday_str = yesterday_dt.strftime(DEFAULT_SERVER_DATE_FORMAT) reservations_to_checkout = self.env['hotel.reservation'].search([ ('state', 'not in', ['done']), ('checkout', '<', today_str) ]) for res in reservations_to_checkout: res.action_reservation_checkout() reservations = self.env['hotel.reservation'].search([ ('reservation_line_ids.date', 'in', [today_str, yesterday_str]), ('state', 'in', ['confirm', 'booking']) ]) self._cr.execute("update hotel_reservation set is_checkin = False, \ is_checkout = False where is_checkin = True or \ is_checkout = True") checkins_res = reservations.filtered(lambda x: ( x.state in ('confirm','draft') and date_utils.date_compare(x.checkin, today_str, hours=False) and x.reservation_type == 'normal')) checkins_res.write({'is_checkin': True}) checkouts_res = reservations.filtered(lambda x: ( x.state not in ('done','cancelled') and date_utils.date_compare(x.checkout, today_str, hours=False) and x.reservation_type == 'normal')) checkouts_res.write({'is_checkout': True}) self.env['hotel.folio'].daily_plan() return True @api.model def checkin_is_today(self): self.ensure_one() date_now_str = date_utils.now().strftime( DEFAULT_SERVER_DATE_FORMAT) return date_utils.date_compare(self.checkin, date_now_str, hours=False) @api.model def checkout_is_today(self): self.ensure_one() date_now_str = date_utils.now().strftime( DEFAULT_SERVER_DATE_FORMAT) return date_utils.date_compare(self.checkout, date_now_str, hours=False) @api.multi def action_cancel(self): for record in self: record.write({ 'state': 'cancelled', 'discount': 100.0, }) if record.checkin_is_today: record.is_checkin = False folio = self.env['hotel.folio'].browse(record.folio_id.id) folio.checkins_reservations = folio.room_lines.search_count([ ('folio_id', '=', folio.id), ('is_checkin', '=', True) ]) if record.splitted: master_reservation = record.parent_reservation or record splitted_reservs = self.env['hotel.reservation'].search([ ('splitted', '=', True), '|', ('parent_reservation', '=', master_reservation.id), ('id', '=', master_reservation.id), ('folio_id', '=', record.folio_id.id), ('id', '!=', record.id), ('state', '!=', 'cancelled') ]) splitted_reservs.action_cancel() record.folio_id.compute_invoices_amount() @api.multi def draft(self): for record in self: record.write({'state': 'draft'}) if record.splitted: master_reservation = record.parent_reservation or record splitted_reservs = self.env['hotel.reservation'].search([ ('splitted', '=', True), '|', ('parent_reservation', '=', master_reservation.id), ('id', '=', master_reservation.id), ('folio_id', '=', record.folio_id.id), ('id', '!=', record.id), ('state', '!=', 'draft') ]) splitted_reservs.draft() @api.multi def action_reservation_checkout(self): for record in self: record.state = 'done' if record.checkout_is_today(): record.is_checkout = False folio = self.env['hotel.folio'].browse(self.folio_id.id) folio.checkouts_reservations = folio.room_lines.search_count([ ('folio_id', '=', folio.id), ('is_checkout', '=', True) ]) @api.multi def overbooking_button(self): self.ensure_one() return self.write({'overbooking': not self.overbooking}) @api.multi def open_master(self): self.ensure_one() if not self.parent_reservation: raise ValidationError(_("This is the parent reservation")) action = self.env.ref('hotel.open_hotel_reservation_form_tree_all').read()[0] action['views'] = [(self.env.ref('hotel.view_hotel_reservation_form').id, 'form')] action['res_id'] = self.parent_reservation.id return action @api.multi def open_folio(self): action = self.env.ref('hotel.open_hotel_folio1_form_tree_all').read()[0] if self.folio_id: action['views'] = [(self.env.ref('hotel.view_hotel_folio1_form').id, 'form')] action['res_id'] = self.folio_id.id else: action = {'type': 'ir.actions.act_window_close'} return action @api.multi def open_reservation_form(self): action = self.env.ref('hotel.open_hotel_reservation_form_tree_all').read()[0] action['views'] = [(self.env.ref('hotel.view_hotel_reservation_form').id, 'form')] action['res_id'] = self.id return action @api.multi def get_real_checkin_checkout(self): self.ensure_one() if not self.splitted: return (self.checkin, self.checkout) master_reservation = self.parent_reservation or self splitted_reservs = self.env['hotel.reservation'].search([ ('splitted', '=', True), ('folio_id', '=', self.folio_id.id), '|', ('parent_reservation', '=', master_reservation.id), ('id', '=', master_reservation.id) ]) last_checkout = splitted_reservs[0].checkout first_checkin = splitted_reservs[0].checkin for reserv in splitted_reservs: if last_checkout < reserv.checkout: last_checkout = reserv.checkout if first_checkin > reserv.checkin: first_checkin = reserv.checkin return (first_checkin, last_checkout) @api.multi def unify(self): # FIXME Remove product inheritance pass # self.ensure_one() # if not self.splitted: # raise ValidationError(_("This reservation can't be unified")) # # master_reservation = self.parent_reservation or self # self_is_master = (master_reservation == self) # # splitted_reservs = self.env['hotel.reservation'].search([ # ('splitted', '=', True), # ('folio_id', '=', self.folio_id.id), # '|', # ('parent_reservation', '=', master_reservation.id), # ('id', '=', master_reservation.id) # ]) # # rooms_products = splitted_reservs.mapped('product_id.id') # if len(rooms_products) > 1 or \ # (len(rooms_products) == 1 # and master_reservation.product_id.id != rooms_products[0]): # raise ValidationError(_("This reservation can't be unified: They \ # all need to be in the same room")) # # # Search checkout # last_checkout = splitted_reservs[0].checkout # for reserv in splitted_reservs: # if last_checkout < reserv.checkout: # last_checkout = reserv.checkout # # # 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)] # tprice = 0.0 # for rline in reservation_line_ids: # rlines.append((0, False, { # 'date': rline.date, # 'price': rline.price, # })) # tprice += rline.price # # # Unify # folio = self.folio_id # FIX: To Allow Unify confirm reservations # state = folio.state # FIX # folio.state = 'draft' # FIX # osplitted_reservs = splitted_reservs - master_reservation # osplitted_reservs.sudo().unlink() # folio.state = state # FIX # # # FIXME: Two writes because checkout regenerate reservation lines # master_reservation.write({ # 'checkout': last_checkout, # 'splitted': False, # }) # master_reservation.write({ # 'reservation_line_ids': rlines, # 'price_unit': tprice, # }) # if not self_is_master: # return {'type': 'ir.actions.act_window_close'} # return True # # ''' # Created this because "copy()" function create a new record # and collide with date restrictions. # This function generate a usable dictionary with reservation values # for copy purposes. # ''' @api.multi def generate_copy_values(self, checkin=False, checkout=False): self.ensure_one() return { 'name': self.name, 'adults': self.adults, 'children': self.children, 'checkin': checkin or self.checkin, 'checkout': checkout or self.checkout, 'folio_id': self.folio_id.id, # 'product_id': self.product_id.id, 'parent_reservation': self.parent_reservation.id, 'state': self.state, 'overbooking': self.overbooking, 'price_unit': self.price_unit, 'splitted': self.splitted, # 'virtual_room_id': self.virtual_room_id.id, 'room_type_id': self.room_type_id.id, } @api.model def create(self, vals): """ Overrides orm create method. @param self: The object pointer @param vals: dictionary of fields value. @return: new record set for hotel folio line. """ # import wdb; wdb.set_trace() if not 'reservation_type' in vals or not vals.get('reservation_type'): vals.update({'reservation_type': 'normal'}) if 'folio_id' in vals: folio = self.env["hotel.folio"].browse(vals['folio_id']) # vals.update({'order_id': folio.order_id.id, # 'channel_type': folio.channel_type}) vals.update({'channel_type': folio.channel_type}) elif 'partner_id' in vals: folio_vals = {'partner_id':int(vals.get('partner_id')), 'channel_type': vals.get('channel_type')} folio = self.env["hotel.folio"].create(folio_vals) # vals.update({'order_id': folio.order_id.id, # 'folio_id': folio.id, # 'reservation_type': vals.get('reservation_type'), # 'channel_type': vals.get('channel_type')}) vals.update({'folio_id': folio.id, 'reservation_type': vals.get('reservation_type'), 'channel_type': vals.get('channel_type')}) user = self.env['res.users'].browse(self.env.uid) if user.has_group('hotel.group_hotel_call'): vals.update({'to_assign': True, 'channel_type': 'call'}) vals.update({ 'last_updated_res': date_utils.now(hours=True).strftime(DEFAULT_SERVER_DATETIME_FORMAT) }) if folio: record = super(HotelReservation, self).create(vals) # Check Capacity # NOTE the room is not a product anymore # room = self.env['hotel.room'].search([ # ('product_id', '=', record.product_id.id) # ]) #persons = record.adults # Not count childrens if record.adults > record.room_id.capacity: raise ValidationError( _("Reservation persons can't be higher than room capacity")) if record.adults == 0: raise ValidationError(_("Reservation has no adults")) if (record.state == 'draft' and record.folio_id.state == 'sale') or \ record.preconfirm: record.confirm() record._compute_color() return record @api.multi def write(self, vals): for record in self: if ('checkin' in vals and record.checkin != vals['checkin']) or \ ('checkout' in vals and record.checkout != vals['checkout']) or \ ('state' in vals and record.state != vals['state']) or \ ('amount_discount' in vals and record.amount_discount != vals['amount_discount']): vals.update({'to_send': True}) pricesChanged = ('checkin' in vals or \ 'checkout' in vals or \ 'discount' in vals) # vals.update({ # 'edit_room': False, # }) # if pricesChanged or 'state' in vals or 'virtual_room_id' in vals or 'to_assign' in vals: if pricesChanged or 'state' in vals or 'room_type_id' in vals or 'to_assign' in vals: vals.update({ 'last_updated_res': date_utils.now(hours=True).strftime(DEFAULT_SERVER_DATETIME_FORMAT) }) user = self.env['res.users'].browse(self.env.uid) if user.has_group('hotel.group_hotel_call'): vals.update({ 'to_read': True, 'to_assign': True, }) res = super(HotelReservation, self).write(vals) if pricesChanged: for record in self: if record.reservation_type in ('staff', 'out'): record.update({'price_unit': 0}) record.folio_id.compute_invoices_amount() checkin = vals.get('checkin', record.checkin) checkout = vals.get('checkout', record.checkout) days_diff = date_utils.date_diff(checkin, checkout, hours=False) rlines = record.prepare_reservation_lines(checkin, days_diff) record.update({ 'reservation_line_ids': rlines['commands'], 'price_unit': rlines['total_price'], }) return res # @api.multi # def uos_change(self, product_uos, product_uos_qty=0, product_id=None): # ''' # @param self: object pointer # ''' # # for folio in self: # # line = folio.order_line_id # # line.uos_change(product_uos, product_uos_qty=0, # # product_id=None) # return True # FIXME add room.id to on change after removing inheritance @api.onchange('adults', 'children') def check_capacity(self): if self.room_id: persons = self.adults + self.children if self.room_id.capacity < persons: self.adults = self.room_id.capacity self.children = 0 raise UserError( _('%s people do not fit in this room! ;)') % (persons)) @api.onchange('room_type_id') # def on_change_virtual_room_id(self): def on_change_room_type_id(self): if not self.checkin: self.checkin = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) if not self.checkout: self.checkout = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) days_diff = date_utils.date_diff( self.checkin, self.checkout, hours=False) rlines = self.prepare_reservation_lines( self.checkin, days_diff, update_old_prices=True) self.reservation_line_ids = rlines['commands'] if self.reservation_type in ['staff', 'out']: self.price_unit = 0.0 self.cardex_pending = 0 else: self.price_unit = rlines['total_price'] @api.onchange('checkin', 'checkout', 'room_id', 'reservation_type', 'room_type_id') def on_change_checkin_checkout_product_id(self): _logger.info('on_change_checkin_checkout_product_id') # import wdb; wdb.set_trace() if not self.checkin: self.checkin = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) if not self.checkout: self.checkout = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) # WARNING Need a review # if self.product_id: # self.tax_id = [(6, False, self.virtual_room_id.product_id.taxes_id.ids)] # room = self.env['hotel.room'].search([ # ('product_id', '=', self.product_id.id) # ]) # if self.adults == 0: # self.adults = room.capacity # if not self.virtual_room_id and room.price_virtual_room: # self.virtual_room_id = room.price_virtual_room.id if self.room_id: # self.tax_id = [(6, False, self.room_type_id.product_id.taxes_id.ids)] if self.adults == 0: self.adults = self.room_id.capacity if not self.room_type_id: self.room_type_id = self.room_id.room_type_id self.tax_id = [(6, False, self.room_id.room_type_id.taxes_id.ids)] # UTC -> Hotel tz tz = self.env['ir.default'].sudo().get('res.config.settings', 'tz_hotel') chkin_utc_dt = date_utils.get_datetime(self.checkin) chkout_utc_dt = date_utils.get_datetime(self.checkout) if self.room_type_id: checkin_str = chkin_utc_dt.strftime('%d/%m/%Y') checkout_str = chkout_utc_dt.strftime('%d/%m/%Y') self.name = self.room_type_id.name + ': ' + checkin_str + ' - '\ + checkout_str # self.product_uom = self.product_id.uom_id if chkin_utc_dt >= chkout_utc_dt: dpt_hour = self.env['ir.default'].sudo().get( 'res.config.settings', 'default_departure_hour') checkout_str = (chkin_utc_dt + timedelta(days=1)).strftime( DEFAULT_SERVER_DATE_FORMAT) checkout_str = "%s %s:00" % (checkout_str, dpt_hour) checkout_dt = date_utils.get_datetime(checkout_str, stz=tz) checkout_utc_dt = date_utils.dt_as_timezone(checkout_dt, 'UTC') self.checkout = checkout_utc_dt.strftime( DEFAULT_SERVER_DATETIME_FORMAT) if self.state == 'confirm' and self.checkin_is_today(): self.is_checkin = True folio = self.env['hotel.folio'].browse(self.folio_id.id) if folio: folio.checkins_reservations = folio.room_lines.search_count([ ('folio_id', '=', folio.id), ('is_checkin', '=', True) ]) if self.state == 'booking' and self.checkout_is_today(): self.is_checkout = False folio = self.env['hotel.folio'].browse(self.folio_id.id) if folio: folio.checkouts_reservations = folio.room_lines.search_count([ ('folio_id', '=', folio.id), ('is_checkout', '=', True) ]) days_diff = date_utils.date_diff( self.checkin, self.checkout, hours=False) rlines = self.prepare_reservation_lines( self.checkin, days_diff, update_old_prices=False) self.reservation_line_ids = rlines['commands'] if self.reservation_type in ['staff', 'out']: self.price_unit = 0.0 self.cardex_pending = 0 else: self.price_unit = rlines['total_price'] # FIXME add room.id to on change after removing inheritance @api.model def get_availability(self, checkin, checkout, dbchanged=True, dtformat=DEFAULT_SERVER_DATE_FORMAT): date_start = date_utils.get_datetime(checkin) date_end = date_utils.get_datetime(checkout) # Not count end day of the reservation date_diff = date_utils.date_diff(date_start, date_end, hours=False) hotel_vroom_obj = self.env['hotel.room.type'] # virtual_room_avail_obj = self.env['hotel.room.type.availability'] rooms_avail = [] # FIXME con una relacion Many2one, cada habitacion está en un solo tipo # por lo que la disponibilidad para la habitación se tiene que buscar # directamente en ese tipo # vrooms = hotel_vroom_obj.search([ # ('room_ids.product_id', '=', self.room_id) # ]) # FIXME Si lo de arriba es cierto, este bucle sobra. Sólo hay un room_type_id for vroom in self.room_type_id: rdays = [] for i in range(0, date_diff): ndate_dt = date_start + timedelta(days=i) ndate_str = ndate_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) avail = len(hotel_vroom_obj.check_availability_virtual_room( ndate_str, ndate_str, room_type_id=vroom.id)) if not dbchanged: avail = avail - 1 # Can be less than zero because 'avail' can not equal # with the real 'avail' (ex. Online Limits) avail = max(min(avail, vroom.total_rooms_count), 0) rdays.append({ 'date': ndate_dt.strftime(dtformat), 'avail': avail, }) ravail = {'id': vroom.id, 'days': rdays} rooms_avail.append(ravail) return rooms_avail @api.multi def prepare_reservation_lines(self, str_start_date_utc, days, update_old_prices=False): self.ensure_one() total_price = 0.0 cmds = [(5, False, False)] # TO-DO: Redesign relation between hotel.reservation # and sale.order.line to allow manage days by units in order #~ if self.invoice_status == 'invoiced' and not self.splitted: #~ raise ValidationError(_("This reservation is already invoiced. \ #~ To expand it you must create a new reservation.")) hotel_tz = self.env['ir.default'].sudo().get( 'res.config.settings', 'hotel_tz') start_date_utc_dt = date_utils.get_datetime(str_start_date_utc) start_date_dt = date_utils.dt_as_timezone(start_date_utc_dt, hotel_tz) # import wdb; wdb.set_trace() # room = self.env['hotel.room'].search([ # ('product_id', '=', self.product_id.id) # ]) # product_id = self.room_id.sale_price_type == 'vroom' and self.room_id.price_virtual_room.product_id product_id = self.room_type_id pricelist_id = self.env['ir.default'].sudo().get( 'res.config.settings', 'parity_pricelist_id') if pricelist_id: pricelist_id = int(pricelist_id) old_lines_days = self.mapped('reservation_line_ids.date') for i in range(0, days): ndate = start_date_dt + timedelta(days=i) ndate_str = ndate.strftime(DEFAULT_SERVER_DATE_FORMAT) _logger.info('ndate_str: %s', ndate_str) if update_old_prices or ndate_str not in old_lines_days: # prod = product_id.with_context( # lang=self.partner_id.lang, # partner=self.partner_id.id, # quantity=1, # date=ndate_str, # pricelist=pricelist_id, # uom=self.product_uom.id) prod = product_id.with_context( lang=self.partner_id.lang, partner=self.partner_id.id, quantity=1, date=ndate_str, pricelist=pricelist_id) line_price = prod.price else: line = self.reservation_line_ids.filtered(lambda r: r.date == ndate_str) line_price = line.price cmds.append((0, False, { 'date': ndate_str, 'price': line_price })) total_price += line_price return {'total_price': total_price, 'commands': cmds} @api.constrains('adults') def check_adults(self): if self.adults == 0 and self.room_id: # room = self.env['hotel.room'].search([ # ('product_id', '=', self.product_id.id) # ], limit=1) self.adults = self.room_id.capacity @api.multi @api.onchange('checkin', 'checkout', 'room_type_id', 'room_id') def on_change_checkout(self): ''' When you change checkin or checkout it will checked it and update the qty of hotel folio line ----------------------------------------------------------------- @param self: object pointer ''' _logger.info('on_change_checkout') self.ensure_one() now_utc_dt = date_utils.now() if not self.checkin: self.checkin = now_utc_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) if not self.checkout: now_utc_dt = date_utils.get_datetime(self.checkin)\ + timedelta(days=1) self.checkout = now_utc_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) if self.overbooking: return checkout_dt = date_utils.get_datetime(self.checkout) occupied = self.env['hotel.reservation'].occupied( self.checkin, checkout_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)).filtered( lambda r: r.id != self._origin.id) rooms_occupied = occupied.mapped('room_id.id') if self.room_id and self.room_id.id in rooms_occupied: warning_msg = _('You tried to change \ reservation with room those already reserved in this \ reservation period') raise ValidationError(warning_msg) domain_rooms = [ # ('isroom', '=', True), ('id', 'not in', rooms_occupied) ] # if self.check_rooms: # if self.room_type_id: # domain_rooms.append( # ('categ_id.id', '=', self.room_type_id.cat_id.id) # ) # if self.virtual_room_id: # room_categories = self.virtual_room_id.room_type_ids.mapped( # 'cat_id.id') # link_virtual_rooms = self.virtual_room_id.room_ids\ # | self.env['hotel.room'].search([ # ('categ_id.id', 'in', room_categories)]) # room_ids = link_virtual_rooms.mapped('room_id.id') # domain_rooms.append(('id', 'in', room_ids)) return {'domain': {'room_id': domain_rooms}} @api.multi def confirm(self): ''' @param self: object pointer ''' _logger.info('confirm') hotel_folio_obj = self.env['hotel.folio'] hotel_reserv_obj = self.env['hotel.reservation'] for r in self: vals = {} if r.cardex_ids: vals.update({'state': 'booking'}) else: vals.update({'state': 'confirm'}) if r.checkin_is_today(): vals.update({'is_checkin': True}) folio = hotel_folio_obj.browse(r.folio_id.id) folio.checkins_reservations = folio.room_lines.search_count([ ('folio_id', '=', folio.id), ('is_checkin', '=', True)]) r.write(vals) if r.splitted: master_reservation = r.parent_reservation or r splitted_reservs = hotel_reserv_obj.search([ ('splitted', '=', True), '|', ('parent_reservation', '=', master_reservation.id), ('id', '=', master_reservation.id), ('folio_id', '=', r.folio_id.id), ('id', '!=', r.id), ('state', '!=', 'confirm') ]) splitted_reservs.confirm() return True @api.multi def button_done(self): ''' @param self: object pointer ''' for res in self: res.action_reservation_checkout() return True @api.one def copy_data(self, default=None): ''' @param self: object pointer @param default: dict of default values to be set ''' return False # FIXME added for migration # line_id = self.order_line_id.id # sale_line_obj = self.env['sale.order.line'].browse(line_id) # return sale_line_obj.copy_data(default=default) @api.constrains('checkin', 'checkout', 'state', 'room_id', 'overbooking') def check_dates(self): """ 1.-When date_order is less then checkin date or Checkout date should be greater than the checkin date. 3.-Check the reservation dates are not occuped """ chkin_utc_dt = date_utils.get_datetime(self.checkin) chkout_utc_dt = date_utils.get_datetime(self.checkout) if chkin_utc_dt >= chkout_utc_dt: raise ValidationError(_('Room line Check In Date Should be \ less than the Check Out Date!')) if not self.overbooking and not self._context.get("ignore_avail_restrictions", False): occupied = self.env['hotel.reservation'].occupied( self.checkin, chkout_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)) occupied = occupied.filtered( lambda r: r.room_id.id == self.room_id.id and r.id != self.id) occupied_name = ','.join(str(x.room_id.name) for x in occupied) if occupied: warning_msg = _('You tried to change/confirm \ reservation with room those already reserved in this \ reservation period: %s ') % occupied_name raise ValidationError(warning_msg) @api.multi def unlink(self): # for record in self: # record.order_line_id.unlink() return super(HotelReservation, self).unlink() @api.model def occupied(self, str_checkin_utc, str_checkout_utc): """ Return a RESERVATIONS array between in and out parameters IMPORTANT: This function should receive the dates in UTC datetime zone, as String format """ tz_hotel = self.env['ir.default'].sudo().get( 'res.config.settings', 'tz_hotel') checkin_utc_dt = date_utils.get_datetime(str_checkin_utc) checkin_dt = date_utils.dt_as_timezone(checkin_utc_dt, tz_hotel) days_diff = date_utils.date_diff(str_checkin_utc, str_checkout_utc, hours=False) dates_list = date_utils.generate_dates_list(checkin_dt, days_diff or 1, stz=tz_hotel) reservations = self.env['hotel.reservation'].search([ ('reservation_line_ids.date', 'in', dates_list), ('state', '!=', 'cancelled'), ('overbooking', '=', False) ]) return reservations