diff --git a/.gitignore b/.gitignore index be0ddc715..416d59a46 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .settings/ +.idea diff --git a/hotel/i18n/es.po b/hotel/i18n/es.po index a10548771..9e4b9e51c 100644 --- a/hotel/i18n/es.po +++ b/hotel/i18n/es.po @@ -4842,7 +4842,7 @@ msgid "Invoices" msgstr "Facturas" #. module: hotel -#: model:ir.model.fields,field_description:hotel.field_hotel_folio_invoices_amount +#: model:ir.model.fields,field_description:hotel.field_hotel_folio_pending_amount #: model:ir.model.fields,field_description:hotel.field_hotel_reservation_folio_pending_amount msgid "Invoices amount" msgstr "Cantidad Facturada" diff --git a/hotel/models/hotel_folio.py b/hotel/models/hotel_folio.py index 54dd4fbe8..528777354 100644 --- a/hotel/models/hotel_folio.py +++ b/hotel/models/hotel_folio.py @@ -23,50 +23,6 @@ from odoo.addons import decimal_precision as dp class HotelFolio(models.Model): - @api.model - def name_search(self, name='', args=None, operator='ilike', limit=100): - if args is None: - args = [] - args += ([('name', operator, name)]) - mids = self.search(args, limit=100) - return mids.name_get() - - @api.model - def _needaction_count(self, domain=None): - """ - Show a count of draft state folio on the menu badge. - @param self: object pointer - """ - return self.search_count([('state', '=', 'draft')]) - - @api.multi - def copy(self, default=None): - ''' - @param self: object pointer - @param default: dict of default values to be set - ''' - return super(HotelFolio, self).copy(default=default) - - @api.multi - def _invoiced(self, name, arg): - ''' - @param self: object pointer - @param name: Names of fields. - @param arg: User defined arguments - ''' - pass - # return self.env['sale.order']._invoiced(name, arg) - - @api.multi - def _invoiced_search(self, obj, name, args): - ''' - @param self: object pointer - @param name: Names of fields. - @param arg: User defined arguments - ''' - pass - # return self.env['sale.order']._invoiced_search(obj, name, args) - # @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity') def _get_invoice_qty(self): pass @@ -93,7 +49,7 @@ class HotelFolio(models.Model): _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] name = fields.Char('Folio Number', readonly=True, index=True, - default='New') + default=lambda self: _('New')) partner_id = fields.Many2one('res.partner', track_visibility='onchange') # partner_invoice_id = fields.Many2one('res.partner', @@ -108,6 +64,7 @@ class HotelFolio(models.Model): mobile = fields.Char('Mobile', related='partner_id.mobile') phone = fields.Char('Phone', related='partner_id.phone') + #Review: How to use state in folio? state = fields.Selection([('draft', 'Pre-reservation'), ('confirm', 'Pending Entry'), ('booking', 'On Board'), ('done', 'Out'), ('cancelled', 'Cancelled')], @@ -126,37 +83,27 @@ class HotelFolio(models.Model): help="Hotel services detail provide to " "customer and it will include in " "main Invoice.") - # service_line_ids = fields.One2many('hotel.service.line', 'folio_id', - # readonly=False, - # states={'done': [('readonly', True)]}, - # help="Hotel services detail provide to" - # "customer and it will include in " - # "main Invoice.") - # has no sense used as this way hotel_invoice_id = fields.Many2one('account.invoice', 'Invoice') company_id = fields.Many2one('res.company', 'Company') - # currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', - # string='Currency', readonly=True, required=True) + currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', + string='Currency', readonly=True, required=True) - # pricelist_id = fields.Many2one('product.pricelist', - # string='Pricelist', - # required=True, - # readonly=True, - # states={'draft': [('readonly', False)], - # 'sent': [('readonly', False)]}, - # help="Pricelist for current sales order.") - # Monetary to Float - invoices_amount = fields.Float(compute='compute_invoices_amount', + pricelist_id = fields.Many2one('product.pricelist', + string='Pricelist', + required=True, + readonly=True, + states={'draft': [('readonly', False)], + 'sent': [('readonly', False)]}, + help="Pricelist for current folio.") + pending_amount = fields.Monetary(compute='compute_amount', store=True, string="Pending in Folio") - # Monetary to Float - refund_amount = fields.Float(compute='compute_invoices_amount', + refund_amount = fields.Monetary(compute='compute_amount', store=True, string="Payment Returns") - # Monetary to Float - invoices_paid = fields.Float(compute='compute_invoices_amount', + invoices_paid = fields.Monetary(compute='compute_amount', store=True, track_visibility='onchange', string="Payments") @@ -228,75 +175,27 @@ class HotelFolio(models.Model): amount_total = fields.Float(string='Total', store=True, readonly=True, track_visibility='always') - - def _compute_fix_price(self): - for record in self: - for res in record.room_lines: - if res.fix_total == True: - record.fix_price = True - break - else: - record.fix_price = False - - def action_recalcule_payment(self): - for record in self: - for res in record.room_lines: - res.on_change_checkin_checkout_product_id() - def _computed_rooms_char(self): for record in self: rooms = ', '.join(record.mapped('room_lines.room_id.name')) record.rooms_char = rooms - @api.model - def recompute_amount(self): - folios = self.env['hotel.folio'] - if folios: - folios = folios.filtered(lambda x: ( - x.name == folio_name)) - folios.compute_invoices_amount() - @api.multi def _compute_num_invoices(self): pass # for fol in self: # fol.num_invoices = len(self.mapped('invoice_ids.id')) - @api.model - def daily_plan(self): - _logger.info('daily_plan') - self._cr.execute("update hotel_folio set checkins_reservations = 0, \ - checkouts_reservations = 0 where checkins_reservations > 0 \ - or checkouts_reservations > 0") - folios_in = self.env['hotel.folio'].search([ - ('room_lines.is_checkin', '=', True) - ]) - folios_out = self.env['hotel.folio'].search([ - ('room_lines.is_checkout', '=', True) - ]) - for fol in folios_in: - count_checkin = fol.room_lines.search_count([ - ('is_checkin', '=', True), ('folio_id.id', '=', fol.id) - ]) - fol.write({'checkins_reservations': count_checkin}) - for fol in folios_out: - count_checkout = fol.room_lines.search_count([ - ('is_checkout', '=', True), - ('folio_id.id', '=', fol.id) - ]) - fol.write({'checkouts_reservations': count_checkout}) - return True - # @api.depends('order_line.price_total', 'payment_ids', 'return_ids') @api.multi - def compute_invoices_amount(self): - _logger.info('compute_invoices_amount') + def compute_amount(self): + _logger.info('compute_amount') @api.multi def action_pay(self): self.ensure_one() partner = self.partner_id.id - amount = self.invoices_amount + amount = self.pending_amount view_id = self.env.ref('hotel.view_account_payment_folio_form').id return{ 'name': _('Register Payment'), @@ -373,6 +272,178 @@ class HotelFolio(models.Model): 'domain': [('id', 'in', return_move_ids)], } + @api.multi + def action_folios_amount(self): + now_utc_dt = date_utils.now() + now_utc_str = now_utc_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + reservations = self.env['hotel.reservation'].search([ + ('checkout', '<=', now_utc_str) + ]) + folio_ids = reservations.mapped('folio_id.id') + folios = self.env['hotel.folio'].search([('id', 'in', folio_ids)]) + folios = folios.filtered(lambda r: r.pending_amount > 0) + return { + 'name': _('Pending'), + 'view_type': 'form', + 'view_mode': 'tree,form', + 'res_model': 'hotel.folio', + 'type': 'ir.actions.act_window', + 'domain': [('id', 'in', folios.ids)] + } + + @api.multi + def go_to_currency_exchange(self): + ''' + when Money Exchange button is clicked then this method is called. + ------------------------------------------------------------------- + @param self: object pointer + ''' + _logger.info('go_to_currency_exchange') + pass + # cr, uid, context = self.env.args + # context = dict(context) + # for rec in self: + # if rec.partner_id.id and len(rec.room_lines) != 0: + # context.update({'folioid': rec.id, 'guest': rec.partner_id.id, + # 'room_no': rec.room_lines[0].product_id.name}) + # self.env.args = cr, uid, misc.frozendict(context) + # else: + # raise except_orm(_('Warning'), _('Please Reserve Any Room.')) + # return {'name': _('Currency Exchange'), + # 'res_model': 'currency.exchange', + # 'type': 'ir.actions.act_window', + # 'view_id': False, + # 'view_mode': 'form,tree', + # 'view_type': 'form', + # 'context': {'default_folio_no': context.get('folioid'), + # 'default_hotel_id': context.get('hotel'), + # 'default_guest_name': context.get('guest'), + # 'default_room_number': context.get('room_no') + # }, + # } + + @api.model + def create(self, vals, check=True): + if vals.get('name', _('New')) == _('New'): + if 'company_id' in vals: + vals['name'] = self.env['ir.sequence'].with_context(force_company=vals['company_id']).next_by_code('sale.order') or _('New') + else: + vals['name'] = self.env['ir.sequence'].next_by_code('hotel.folio') or _('New') + + # Makes sure partner_invoice_id' and 'pricelist_id' are defined + if any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']): + partner = self.env['res.partner'].browse(vals.get('partner_id')) + addr = partner.address_get(['delivery', 'invoice']) + vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice']) + vals['pricelist_id'] = vals.setdefault('pricelist_id', partner.property_product_pricelist and partner.property_product_pricelist.id) + result = super(HotelFolio, self).create(vals) + return result + + @api.multi + @api.onchange('partner_id') + def onchange_partner_id(self): + """ + Update the following fields when the partner is changed: + - Pricelist + - Invoice address + - user_id + """ + if not self.partner_id: + self.update({ + 'partner_invoice_id': False, + 'payment_term_id': False, + 'fiscal_position_id': False, + }) + return + + addr = self.partner_id.address_get(['invoice']) + values = { + 'pricelist_id': self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.id or False, + 'partner_invoice_id': addr['invoice'], + 'user_id': self.partner_id.user_id.id or self.env.uid + } + if self.env['ir.config_parameter'].sudo().get_param('sale.use_sale_note') and self.env.user.company_id.sale_note: + values['note'] = self.with_context(lang=self.partner_id.lang).env.user.company_id.sale_note + + if self.partner_id.team_id: + values['team_id'] = self.partner_id.team_id.id + self.update(values) + + @api.multi + def action_invoice_create(self, grouped=False, states=None): + ''' + @param self: object pointer + ''' + pass + # if states is None: + # states = ['confirmed', 'done'] + # order_ids = [folio.order_id.id for folio in self] + # sale_obj = self.env['sale.order'].browse(order_ids) + # invoice_id = (sale_obj.action_invoice_create(grouped=False, + # states=['confirmed', + # 'done'])) + # for line in self: + # values = {'invoiced': True, + # 'state': 'progress' if grouped else 'progress', + # 'hotel_invoice_id': invoice_id + # } + # line.write(values) + # return invoice_id + + @api.multi + def advance_invoice(self): + pass + + ''' + WORKFLOW STATE + ''' + + @api.multi + def button_dummy(self): + ''' + @param self: object pointer + ''' + # for folio in self: + # folio.order_id.button_dummy() + return True + + @api.multi + def action_done(self): + for line in self.room_lines: + if line.state == "booking": + line.action_reservation_checkout() + + @api.multi + def action_cancel(self): + ''' + @param self: object pointer + ''' + pass + # for sale in self: + # if not sale.order_id: + # raise ValidationError(_('Order id is not available')) + # for invoice in sale.invoice_ids: + # invoice.state = 'cancel' + # sale.room_lines.action_cancel() + # sale.order_id.action_cancel() + + @api.multi + def print_quotation(self): + pass + # TODO- New report to reservation order + # self.order_id.filtered(lambda s: s.state == 'draft').write({ + # 'state': 'sent', + # }) + # return self.env.ref('sale.report_saleorder').report_action(self, data=data) + + @api.multi + def action_confirm(self): + _logger.info('action_confirm') + + + """ + CHECKIN/OUT PROCESS + """ @api.multi def action_checks(self): self.ensure_one() @@ -386,25 +457,59 @@ class HotelFolio(models.Model): 'domain': [('reservation_id', 'in', rooms)], 'target': 'new', } + + @api.model + def daily_plan(self): + _logger.info('daily_plan') + self._cr.execute("update hotel_folio set checkins_reservations = 0, \ + checkouts_reservations = 0 where checkins_reservations > 0 \ + or checkouts_reservations > 0") + folios_in = self.env['hotel.folio'].search([ + ('room_lines.is_checkin', '=', True) + ]) + folios_out = self.env['hotel.folio'].search([ + ('room_lines.is_checkout', '=', True) + ]) + for fol in folios_in: + count_checkin = fol.room_lines.search_count([ + ('is_checkin', '=', True), ('folio_id.id', '=', fol.id) + ]) + fol.write({'checkins_reservations': count_checkin}) + for fol in folios_out: + count_checkout = fol.room_lines.search_count([ + ('is_checkout', '=', True), + ('folio_id.id', '=', fol.id) + ]) + fol.write({'checkouts_reservations': count_checkout}) + return True @api.multi - def action_folios_amount(self): - now_utc_dt = date_utils.now() - now_utc_str = now_utc_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - reservations = self.env['hotel.reservation'].search([ - ('checkout', '<=', now_utc_str) - ]) - folio_ids = reservations.mapped('folio_id.id') - folios = self.env['hotel.folio'].search([('id', 'in', folio_ids)]) - folios = folios.filtered(lambda r: r.invoices_amount > 0) - return { - 'name': _('Pending'), - 'view_type': 'form', - 'view_mode': 'tree,form', - 'res_model': 'hotel.folio', - 'type': 'ir.actions.act_window', - 'domain': [('id', 'in', folios.ids)] - } + def _compute_cardex_count(self): + _logger.info('_compute_cardex_amount') + for fol in self: + num_cardex = 0 + pending = False + if fol.reservation_type == 'normal': + for reser in fol.room_lines: + if reser.state != 'cancelled' and \ + not reser.parent_reservation: + num_cardex += len(reser.cardex_ids) + fol.cardex_count = num_cardex + pending = 0 + for reser in fol.room_lines: + if reser.state != 'cancelled' and \ + not reser.parent_reservation: + pending += (reser.adults + reser.children) \ + - len(reser.cardex_ids) + if pending <= 0: + fol.cardex_pending = False + else: + fol.cardex_pending = True + fol.cardex_pending_num = pending + + """ + MAILING PROCESS + """ @api.depends('room_lines') def _compute_has_confirmed_reservations_to_send(self): @@ -468,221 +573,6 @@ class HotelFolio(models.Model): break self.has_checkout_to_send = has_to_send - @api.multi - def _compute_cardex_count(self): - _logger.info('_compute_cardex_amount') - for fol in self: - num_cardex = 0 - pending = False - if fol.reservation_type == 'normal': - for reser in fol.room_lines: - if reser.state != 'cancelled' and \ - not reser.parent_reservation: - num_cardex += len(reser.cardex_ids) - fol.cardex_count = num_cardex - pending = 0 - for reser in fol.room_lines: - if reser.state != 'cancelled' and \ - not reser.parent_reservation: - pending += (reser.adults + reser.children) \ - - len(reser.cardex_ids) - if pending <= 0: - fol.cardex_pending = False - else: - fol.cardex_pending = True - fol.cardex_pending_num = pending - - @api.multi - def go_to_currency_exchange(self): - ''' - when Money Exchange button is clicked then this method is called. - ------------------------------------------------------------------- - @param self: object pointer - ''' - _logger.info('go_to_currency_exchange') - pass - # cr, uid, context = self.env.args - # context = dict(context) - # for rec in self: - # if rec.partner_id.id and len(rec.room_lines) != 0: - # context.update({'folioid': rec.id, 'guest': rec.partner_id.id, - # 'room_no': rec.room_lines[0].product_id.name}) - # self.env.args = cr, uid, misc.frozendict(context) - # else: - # raise except_orm(_('Warning'), _('Please Reserve Any Room.')) - # return {'name': _('Currency Exchange'), - # 'res_model': 'currency.exchange', - # 'type': 'ir.actions.act_window', - # 'view_id': False, - # 'view_mode': 'form,tree', - # 'view_type': 'form', - # 'context': {'default_folio_no': context.get('folioid'), - # 'default_hotel_id': context.get('hotel'), - # 'default_guest_name': context.get('guest'), - # 'default_room_number': context.get('room_no') - # }, - # } - - @api.model - def create(self, vals, check=True): - """ - Overrides orm create method. - @param self: The object pointer - @param vals: dictionary of fields value. - @return: new record set for hotel folio. - """ - _logger.info('create') - if not 'service_line_ids' and 'folio_id' in vals: - tmp_room_lines = vals.get('room_lines', []) - vals['order_policy'] = vals.get('hotel_policy', 'manual') - vals.update({'room_lines': []}) - for line in (tmp_room_lines): - line[2].update({'folio_id': folio_id}) - vals.update({'room_lines': tmp_room_lines}) - folio_id = super(HotelFolio, self).create(vals) - else: - if not vals: - vals = {} - vals['name'] = self.env['ir.sequence'].next_by_code('hotel.folio') - folio_id = super(HotelFolio, self).create(vals) - - return folio_id - - @api.multi - def write(self, vals): - if 'room_lines' in vals and vals['room_lines'][0][2] and 'reservation_line_ids' in vals['room_lines'][0][2] and vals['room_lines'][0][2]['reservation_line_ids'][0][0] == 5: - del vals['room_lines'] - return super(HotelFolio, self).write(vals) - - @api.multi - @api.onchange('partner_id') - def onchange_partner_id(self): - ''' - When you change partner_id it will update the partner_invoice_id, - partner_shipping_id and pricelist_id of the hotel folio as well - --------------------------------------------------------------- - @param self: object pointer - ''' - _logger.info('onchange_partner_id') - pass - # self.update({ - # 'currency_id': self.env.ref('base.main_company').currency_id, - # 'partner_invoice_id': self.partner_id and self.partner_id.id or False, - # 'partner_shipping_id': self.partner_id and self.partner_id.id or False, - # 'pricelist_id': self.partner_id and self.partner_id.property_product_pricelist.id or False, - # }) - # """ - # Warning messajes saved in partner form to folios - # """ - # if not self.partner_id: - # return - # warning = {} - # title = False - # message = False - # partner = self.partner_id - # - # # If partner has no warning, check its company - # if partner.sale_warn == 'no-message' and partner.parent_id: - # partner = partner.parent_id - # - # if partner.sale_warn != 'no-message': - # # Block if partner only has warning but parent company is blocked - # if partner.sale_warn != 'block' and partner.parent_id \ - # and partner.parent_id.sale_warn == 'block': - # partner = partner.parent_id - # title = _("Warning for %s") % partner.name - # message = partner.sale_warn_msg - # warning = { - # 'title': title, - # 'message': message, - # } - # if self.partner_id.sale_warn == 'block': - # self.update({ - # 'partner_id': False, - # 'partner_invoice_id': False, - # 'partner_shipping_id': False, - # 'pricelist_id': False - # }) - # return {'warning': warning} - # - # if warning: - # return {'warning': warning} - - @api.multi - def button_dummy(self): - ''' - @param self: object pointer - ''' - # for folio in self: - # folio.order_id.button_dummy() - return True - - @api.multi - def action_done(self): - for line in self.room_lines: - if line.state == "booking": - line.action_reservation_checkout() - - @api.multi - def action_invoice_create(self, grouped=False, states=None): - ''' - @param self: object pointer - ''' - pass - # if states is None: - # states = ['confirmed', 'done'] - # order_ids = [folio.order_id.id for folio in self] - # sale_obj = self.env['sale.order'].browse(order_ids) - # invoice_id = (sale_obj.action_invoice_create(grouped=False, - # states=['confirmed', - # 'done'])) - # for line in self: - # values = {'invoiced': True, - # 'state': 'progress' if grouped else 'progress', - # 'hotel_invoice_id': invoice_id - # } - # line.write(values) - # return invoice_id - - @api.multi - def advance_invoice(self): - pass - # order_ids = [folio.order_id.id for folio in self] - # sale_obj = self.env['sale.order'].browse(order_ids) - # invoices = action_invoice_create(self, grouped=True) - # return invoices - - @api.multi - def action_cancel(self): - ''' - @param self: object pointer - ''' - pass - # for sale in self: - # if not sale.order_id: - # raise ValidationError(_('Order id is not available')) - # for invoice in sale.invoice_ids: - # invoice.state = 'cancel' - # sale.room_lines.action_cancel() - # sale.order_id.action_cancel() - - - @api.multi - def action_confirm(self): - _logger.info('action_confirm') - - @api.multi - def print_quotation(self): - pass - # self.order_id.filtered(lambda s: s.state == 'draft').write({ - # 'state': 'sent', - # }) - # return self.env.ref('sale.report_saleorder').report_action(self, data=data) - - @api.multi - def action_cancel_draft(self): - _logger.info('action_confirm') - @api.multi def send_reservation_mail(self): ''' @@ -847,12 +737,6 @@ class HotelFolio(models.Model): template_rec.send_mail(reserv_rec.id, force_send=True) return True - @api.multi - def unlink(self): - # for record in self: - # record.order_id.unlink() - return super(HotelFolio, self).unlink() - @api.multi def get_grouped_reservations_json(self, state, import_all=False): self.ensure_one() @@ -882,3 +766,4 @@ class HotelFolio(models.Model): if not founded: info_grouped.append(vals) return sorted(sorted(info_grouped, key=lambda k: k['num'], reverse=True), key=lambda k: k['room_type']['id']) + diff --git a/hotel/models/hotel_reservation.py b/hotel/models/hotel_reservation.py index 97d050f84..225d01af1 100644 --- a/hotel/models/hotel_reservation.py +++ b/hotel/models/hotel_reservation.py @@ -10,6 +10,7 @@ from odoo.tools import ( from odoo import models, fields, api, _ from decimal import Decimal from dateutil.relativedelta import relativedelta +from dateutil import tz from datetime import datetime, timedelta, date from odoo.addons.hotel import date_utils import pytz @@ -21,6 +22,465 @@ from odoo.addons import decimal_precision as dp class HotelReservation(models.Model): + def _get_default_checkin(self): + folio = False + 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') + today = fields.Date.context_today(self.with_context(tz=tz_hotel)) + return fields.Date.from_string(today).strftime(DEFAULT_SERVER_DATE_FORMAT) + + def _get_default_checkout(self): + folio = False + 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') + today = fields.Date.context_today(self.with_context(tz=tz_hotel)) + return (fields.Date.from_string(today) + timedelta(days=1)).strftime(DEFAULT_SERVER_DATE_FORMAT) + + def _get_default_arrival_hour(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].arrival_hour + else: + return default_arrival_hour + + def _get_default_departure_hour(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].departure_hour + else: + return default_departure_hour + + @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), + ('room_id.name', operator, name) + ] + return super(HotelReservation, self).name_search( + name='', args=args, operator='ilike', limit=limit) + + @api.multi + def name_get(self): + result = [] + for res in self: + name = u'%s (%s)' % (res.folio_id.name, res.room_id.name) + result.append((res.id, name)) + return result + + @api.multi + def _computed_shared(self): + # Has this reservation more charges associates in folio?, Yes?, then, this is share folio ;) + 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: + res.nights = (fields.Date.from_string(res.checkout) - fields.Date.from_string(res.checkin)).days + + _name = 'hotel.reservation' + _description = 'Hotel Reservation' + _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] + _order = "last_updated_res desc, name" + + name = fields.Text('Reservation Description', required=True) + + 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.Date('Check In', required=True, + default=_get_default_checkin, + track_visibility='onchange') + checkout = fields.Date('Check Out', required=True, + default=_get_default_checkout, + track_visibility='onchange') + arrival_hour = fields.Char('Arrival Hour', + default=_get_default_arrival_hour, + help="Default Arrival Hour (HH:MM)") + departure_hour = fields.Char('Departure Hour', + default=_get_default_departure_hour, + help="Default Departure Hour (HH:MM)") + 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) + + 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') + folio_pending_amount = fields.Monetary(related='folio_id.pending_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) + 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) + # 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_subtotal = fields.Monetary(string='Subtotal', readonly=True, store=True, compute='_compute_amount_reservation') + price_total = fields.Monetary(string='Total', readonly=True, store=True, compute='_compute_amount_reservation') + price_tax = fields.Float(string='Taxes', readonly=True, store=True, compute='_compute_amount_reservation') + # 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') + + @api.model + def create(self, vals): + vals.update(self._prepare_add_missing_fields(vals)) + if 'folio_id' in vals: + folio = self.env["hotel.folio"].browse(vals['folio_id']) + 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')} + # Create the folio in case of need (To allow to create reservations direct) + folio = self.env["hotel.folio"].create(folio_vals) + vals.update({'folio_id': folio.id, + 'reservation_type': vals.get('reservation_type'), + 'channel_type': vals.get('channel_type')}) + #~ colors = self._generate_color() + vals.update({ + 'last_updated_res': date_utils.now(hours=True).strftime(DEFAULT_SERVER_DATETIME_FORMAT), + #~ 'reserve_color': colors[0], + #~ 'reserve_color_text': colors[1], + }) + if self.compute_price_out_vals(vals): + vals.update(self.env['hotel.reservation'].prepare_reservation_lines(vals)) + record = super(HotelReservation, self).create(vals) + #~ if (record.state == 'draft' and record.folio_id.state == 'sale') or \ + #~ record.preconfirm: + #~ record.confirm() + return record + + @api.multi + def write(self, vals): + if self.notify_update(vals): + vals.update({ + 'last_updated_res': date_utils.now(hours=True).strftime(DEFAULT_SERVER_DATETIME_FORMAT) + }) + for record in self: + if record.compute_price_out_vals(vals): + days_diff = (fields.Date.from_string(record.checkout) - fields.Date.from_string(record.checkin)).days + record.update(record.prepare_reservation_lines( + record.checkin, + days_diff, + vals = vals)) #REVISAR el unlink + 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']) : + vals.update({'to_send': True}) + res = super(HotelReservation, self).write(vals) + return res + + @api.model + def _prepare_add_missing_fields(self, values): + """ Deduce missing required fields from the onchange """ + res = {} + onchange_fields = ['room_id', 'pricelist_id', + 'reservation_type', 'currency_id'] + if values.get('partner_id') and values.get('room_type_id') and any(f not in values for f in onchange_fields): + line = self.new(values) + line.onchange_room_id() + for field in onchange_fields: + if field not in values: + res[field] = line._fields[field].convert_to_write(line[field], line) + return res + + @api.multi + def notify_update(self, vals): + if 'checkin' in vals or \ + 'checkout' in vals or \ + 'discount' in vals or \ + 'state' in vals or \ + 'room_type_id' in vals or \ + 'to_assign' in vals: + return True + return False + + @api.multi + def overbooking_button(self): + self.ensure_one() + return self.write({'overbooking': not self.overbooking}) + + @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 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.constrains('adults') + def _check_adults(self): + for record in self: + 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")) + + """ + ONCHANGES ---------------------------------------------------------- + """ + + @api.onchange('adults', 'room_id') + def onchange_room_id(self): + # TODO: Usar vals y write + if self.room_id: + if self.room_id.capacity < self.adults: + self.adults = self.room_id.capacity + raise UserError( + _('%s people do not fit in this room! ;)') % (persons)) + if self.adults == 0: + self.adults = self.room_id.capacity + if not self.room_type_id: #Si el registro no existe, modificar room_type aunque ya esté establecido + self.room_type_id = self.room_id.room_type_id + + @api.onchange('partner_id') + def onchange_partner_id(self): + #TODO: Change parity pricelist by default pricelist + values = { + 'pricelist_id': self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.id or \ + self.env['ir.default'].sudo().get('hotel.config.settings', 'parity_pricelist_id'), + } + self.update(values) + + # When we need to overwrite the prices even if they were already established + @api.onchange('room_type_id', 'pricelist_id', 'reservation_type') + def onchange_overwrite_price_by_day(self): + if self.room_type_id and self.checkin and self.checkout: + days_diff = (fields.Date.from_string(self.checkout) - fields.Date.from_string(self.checkin)).days + self.update(self.prepare_reservation_lines( + self.checkin, + days_diff, + update_old_prices = True)) + + # When we need to update prices respecting those that were already established + @api.onchange('checkin', 'checkout') + def onchange_dates(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) + checkin_dt = fields.Date.from_string(self.checkin) + checkout_dt = fields.Date.from_string(self.checkout) + if checkin_dt >= checkout_dt: + self.checkout = (fields.Date.from_string(self.checkin) + timedelta(days=1)).strftime(DEFAULT_SERVER_DATE_FORMAT) + if self.room_type_id: + days_diff = (fields.Date.from_string(self.checkout) - fields.Date.from_string(self.checkin)).days + self.update(self.prepare_reservation_lines( + self.checkin, + days_diff, + update_old_prices = False)) + + + + @api.onchange('checkin', 'checkout', 'room_type_id') + def onchange_compute_reservation_description(self): + if self.room_type_id and self.checkin and self.checkout: + checkin_dt = fields.Date.from_string(self.checkin) + checkout_dt = fields.Date.from_string(self.checkout) + checkin_str = checkin_dt.strftime('%d/%m/%Y') + checkout_str = checkout_dt.strftime('%d/%m/%Y') + self.name = self.room_type_id.name + ': ' + checkin_str + ' - '\ + + checkout_str + + @api.multi + @api.onchange('checkin', 'checkout', 'room_id') + def onchange_room_availabiltiy_domain(self): + self.ensure_one() + if self.checkin and self.checkout: + if self.overbooking: + return + occupied = self.env['hotel.reservation'].get_reservations( + self.checkin, + fields.Date.from_string(self.checkout).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 = [ + ('id', 'not in', rooms_occupied) + ] + return {'domain': {'room_id': domain_rooms}} + + """ + COMPUTE RESERVE COLOR ---------------------------------------------- + """ + @api.multi def _generate_color(self): self.ensure_one() @@ -62,7 +522,7 @@ class HotelReservation(models.Model): 'res.config.settings', 'color_letter_pre_reservation') elif self.state == 'confirm': - if self.folio_id.invoices_amount == 0: + if self.folio_id.pending_amount == 0: reserv_color = ir_values_obj.get( 'res.config.settings', 'color_reservation_pay') reserv_color_text = ir_values_obj.get( @@ -73,7 +533,7 @@ class HotelReservation(models.Model): reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_reservation') elif self.state == 'booking': - if self.folio_id.invoices_amount == 0: + if self.folio_id.pending_amount == 0: reserv_color = ir_values_obj.get( 'res.config.settings', 'color_stay_pay') reserv_color_text = ir_values_obj.get( @@ -84,7 +544,7 @@ class HotelReservation(models.Model): reserv_color_text = ir_values_obj.get( 'res.config.settings', 'color_letter_stay') else: - if self.folio_id.invoices_amount == 0: + if self.folio_id.pending_amount == 0: reserv_color = ir_values_obj.get( 'res.config.settings', 'color_checkout') reserv_color_text = ir_values_obj.get( @@ -96,7 +556,7 @@ class HotelReservation(models.Model): 'res.config.settings', 'color_letter_payment_pending') return (reserv_color, reserv_color_text) - @api.depends('state', 'reservation_type', 'folio_id.invoices_amount', 'to_assign') + @api.depends('state', 'reservation_type', 'folio_id.pending_amount', 'to_assign') def _compute_color(self): _logger.info('_compute_color') for rec in self: @@ -119,406 +579,173 @@ class HotelReservation(models.Model): # ]) # splitted_reservs.write({'reserve_color': rec.reserve_color}) + """ + STATE WORKFLOW ----------------------------------------------------- + """ + @api.multi - def copy(self, default=None): + def confirm(self): ''' @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 + _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: - res.cardex_pending = True + 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 - # 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])] + def button_done(self): + ''' + @param self: object pointer + ''' + for res in self: + res.action_reservation_checkout() + return True + + @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_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() + + """ + PRICE PROCESS ------------------------------------------------------ + """ + @api.multi + def compute_price_out_vals(self, vals): + """ + Compute if It is necesary calc price in write/create + """ + if not vals: + vals = {} + if ('reservation_line_ids' not in vals and \ + ('checkout' in vals or 'checkin' in vals or \ + 'room_type_id' in vals or 'pricelist_id' in vals)): + return True + return False + + @api.depends('reservation_line_ids', 'reservation_line_ids.discount', 'tax_id') + def _compute_amount_reservation(self): + """ + Compute the amounts of the reservation. + """ + for line in self: + amount_room = 0 + for day in line.reservation_line_ids: + amount_room += day.price + if amount_room > 0: + product = line.room_type_id.product_id + price = amount_room * (1 - (line.discount or 0.0) / 100.0) + taxes = line.tax_id.compute_all(price, line.currency_id, 1, product=product) + line.update({ + 'price_tax': sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])), + 'price_total': taxes['total_included'], + 'price_subtotal': taxes['total_excluded'], + }) + + @api.multi + def prepare_reservation_lines(self, dfrom, days, vals=False, + update_old_prices=False): + total_price = 0.0 + cmds = [] + if not vals: + vals = {} + pricelist_id = self.env['ir.default'].sudo().get( + 'res.config.settings', 'parity_pricelist_id') + #~ pricelist_id = vals.get('pricelist_id') or self.pricelist_id.id + product = self.env['hotel.room.type'].browse(vals.get('room_type_id') or self.room_type_id.id).product_id + old_lines_days = self.mapped('reservation_line_ids.date') + partner = self.env['res.partner'].browse(vals.get('partner_id') or self.partner_id.id) + total_price = 0 + for i in range(0, days): + idate = (fields.Date.from_string(dfrom) + timedelta(days=i)).strftime(DEFAULT_SERVER_DATE_FORMAT) + old_line = self.reservation_line_ids.filtered(lambda r: r.date == idate) + if update_old_prices or (idate not in old_lines_days): + product = product.with_context( + lang=partner.lang, + partner=partner.id, + quantity=1, + date=idate, + pricelist=pricelist_id, + uom=product.uom_id.id) + line_price = self.env['account.tax']._fix_tax_included_price_company(product.price, product.taxes_id, self.tax_id, self.company_id) + if old_line: + cmds.append((1, old_line.id, { + 'price': line_price + })) + else: + cmds.append((0, False, { + 'date': idate, + 'price': line_price + })) + else: + line_price = old_line.price + cmds.append((4, old_line.id)) + total_price += line_price + return {'reservation_line_ids': cmds} @api.multi def action_pay_folio(self): @@ -551,6 +778,112 @@ class HotelReservation(models.Model): 'target': 'new', } + """ + AVAILABILTY PROCESS ------------------------------------------------ + """ + + @api.model + def get_reservations(self, dfrom, dto): + """ + @param self: The object pointer + @param dfrom: range date from + @param dto: range date to + @return: array with the reservations _confirmed_ between dfrom and dto + """ + domain = [('reservation_line_ids.date', '>=', dfrom), + ('reservation_line_ids.date', '<', dto), + ('state', '!=', 'cancelled'), + ('overbooking', '=', False)] + reservations = self.env['hotel.reservation'].search(domain) + return self.env['hotel.reservation'].search(domain) + + @api.model + def get_reservations_dates(self, dfrom, dto, room_type=False): + """ + @param self: The object pointer + @param dfrom: range date from + @param dto: range date to + @return: dictionary of lists with reservations (a hash of arrays!) + with the reservations dates between dfrom and dto + reservations_dates + {'2018-07-30': [hotel.reservation(29,), hotel.reservation(30,), + hotel.reservation(31,)], + '2018-07-31': [hotel.reservation(22,), hotel.reservation(35,), + hotel.reservation(36,)], + } + """ + domain = [('date', '>=', dfrom), + ('date', '<', dto)] + lines = self.env['hotel.reservation.line'].search(domain) + reservations_dates = {} + for record in lines: + # kumari.net/index.php/programming/programmingcat/22-python-making-a-dictionary-of-lists-a-hash-of-arrays + # reservations_dates.setdefault(record.date,[]).append(record.reservation_id.room_type_id) + reservations_dates.setdefault(record.date, []).append( + [record.reservation_id, record.reservation_id.room_type_id]) + return reservations_dates + + # TODO: Use default values on checkin /checkout is empty + @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 + """ + _logger.info('check_dates') + if fields.Date.from_string(self.checkin) >= fields.Date.from_string(self.checkout): + 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'].get_reservations( + self.checkin, + self.checkout) + 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) + + """ + CHECKIN/OUT PROCESS ------------------------------------------------ + """ + + @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_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.model def daily_plan(self): _logger.info('daily_plan') @@ -595,106 +928,35 @@ class HotelReservation(models.Model): @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) + tz_hotel = self.env['ir.default'].sudo().get( + 'res.config.settings', 'tz_hotel') + today = fields.Date.context_today(self.with_context(tz=tz_hotel)) + return self.checkin == today @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) + tz_hotel = self.env['ir.default'].sudo().get( + 'res.config.settings', 'tz_hotel') + today = fields.Date.context_today(self.with_context(tz=tz_hotel)) + return self.checkout == today @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): + def action_checks(self): self.ensure_one() - return self.write({'overbooking': not self.overbooking}) + 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 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 + """ + RESERVATION SPLITTED ----------------------------------------------- + """ @api.multi def get_real_checkin_checkout(self): @@ -790,509 +1052,30 @@ class HotelReservation(models.Model): # This function generate a usable dictionary with reservation values # for copy purposes. # ''' + @api.multi - def generate_copy_values(self, checkin=False, checkout=False): + def open_master(self): 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, - } + 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.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 + """ + MAILING PROCESS + """ @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 + def send_reservation_mail(self): + return self.folio_id.send_reservation_mail() @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 + def send_exit_mail(self): + return self.folio_id.send_exit_mail() @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}} + def send_cancel_mail(self): + return self.folio_id.send_cancel_mail() - @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 diff --git a/hotel/models/hotel_reservation_line.py b/hotel/models/hotel_reservation_line.py index 256b44cad..a1278bf53 100644 --- a/hotel/models/hotel_reservation_line.py +++ b/hotel/models/hotel_reservation_line.py @@ -20,6 +20,7 @@ ############################################################################## from odoo import models, fields, api, _ from odoo.addons import decimal_precision as dp +from odoo.exceptions import except_orm, UserError, ValidationError class HotelReservationLine(models.Model): _name = "hotel.reservation.line" diff --git a/hotel/models/hotel_room.py b/hotel/models/hotel_room.py index 3c6cd26a8..a0d799cfa 100644 --- a/hotel/models/hotel_room.py +++ b/hotel/models/hotel_room.py @@ -3,7 +3,7 @@ # Copyright 2017 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import models, fields, api, _ - +from odoo.exceptions import ValidationError class HotelRoom(models.Model): """ The rooms for lodging can be for sleeping, usually called rooms, and also @@ -11,18 +11,12 @@ class HotelRoom(models.Model): """ _name = 'hotel.room' _description = 'Hotel Room' - # The record's name - name = fields.Char('Room Name', required=True) - # Used for activate records - active = fields.Boolean('Active', default=True) - # Used for ordering - sequence = fields.Integer('Sequence', default=0) - _order = "sequence, room_type_id, name" - - # each room has only one type (Many2one) + + name = fields.Char('Room Name', required=True) + active = fields.Boolean('Active', default=True) + sequence = fields.Integer('Sequence', default=0) room_type_id = fields.Many2one('hotel.room.type', 'Hotel Room Type') - floor_id = fields.Many2one('hotel.floor', 'Ubication', help='At which floor the room is located.') # TODO Q. Should the amenities be on the Room Type ? - @@ -30,76 +24,19 @@ class HotelRoom(models.Model): 'room_amenities', 'rcateg_id', string='Room Amenities', help='List of room amenities.') - - # default price for this room - list_price = fields.Float(store=True, - string='Room Rate', - help='The room rate is fixed unless a room type' - ' is selected, in which case the rate is taken from' - ' the room type.') - # how to manage the price - # sale_price_type = fields.Selection([ - # ('fixed', 'Fixed Price'), - # ('vroom', 'Room Type'), - # ], 'Price Type', default='fixed', required=True) - # max number of adults and children per room max_adult = fields.Integer('Max Adult') max_child = fields.Integer('Max Child') - # maximum capacity of the room capacity = fields.Integer('Capacity') # FIXME not used to_be_cleaned = fields.Boolean('To be Cleaned', default=False) - shared_room = fields.Boolean('Shared Room', default=False) - description_sale = fields.Text( 'Sale Description', translate=True, help="A description of the Product that you want to communicate to " " your customers. This description will be copied to every Sales " " Order, Delivery Order and Customer Invoice/Credit Note") - - # In case the price is managed from a specific type of room - # price_virtual_room = fields.Many2one( - # 'hotel.virtual.room', - # 'Price Virtual Room', - # help='Price will be based on selected Virtual Room') - - # virtual_rooms = fields.Many2many('hotel.virtual.room', - # string='Virtual Rooms') - # categ_id = fields.Selection([('room', 'Room '), - # ('shared_room', 'Shared Room'), - # ('parking', 'Parking')], - # string='Hotel Lodging Type', - # store=True, default='room') - -# price_virtual_room_domain = fields.Char( -# compute=_compute_price_virtual_room_domain, -# readonly=True, -# store=False, -# ) - -# @api.multi -# @api.depends('categ_id') -# def _compute_price_virtual_room_domain(self): -# for rec in self: -# rec.price_virtual_room_domain = json.dumps( -# ['|', ('room_ids.id', '=', rec.id), ('room_type_ids.cat_id.id', '=', rec.categ_id.id)] -# ) - - # @api.onchange('categ_id') - # def price_virtual_room_domain(self): - # return { - # 'domain': { - # 'price_virtual_room': [ - # '|', ('room_ids.id', '=', self._origin.id), - # ('room_type_ids.cat_id.id', '=', self.categ_id.id) - # ] - # } - # } - - # @api.multi - # def unlink(self): - # for record in self: - # record.product_id.unlink() - # return super(HotelRoom, self).unlink() + @api.constrains('capacity') + def _check_capacity(self): + if self.capacity < 1: + raise ValidationError(_("Room capacity can't be less than one")) diff --git a/hotel/models/hotel_room_type.py b/hotel/models/hotel_room_type.py index 3a889593a..303368d9e 100644 --- a/hotel/models/hotel_room_type.py +++ b/hotel/models/hotel_room_type.py @@ -16,8 +16,6 @@ from odoo.tools import ( from odoo import models, fields, api, _ from odoo.addons.hotel import date_utils -from odoo.addons import decimal_precision as dp - class HotelRoomType(models.Model): """ Before creating a 'room type', you need to consider the following: With the term 'room type' is meant a type of residential accommodation: for @@ -31,8 +29,6 @@ class HotelRoomType(models.Model): product_id = fields.Many2one('product.product', 'Product Room Type', required=True, delegate=True, ondelete='cascade') - # cat_id = fields.Many2one('product.category', 'category', required=True, - # delegate=True, index=True, ondelete='cascade') room_ids = fields.One2many('hotel.room', 'room_type_id', 'Rooms') # TODO Hierarchical relationship for parent-child tree ? @@ -53,68 +49,51 @@ class HotelRoomType(models.Model): 'code must be unique!')] # total number of rooms in this type total_rooms_count = fields.Integer(compute='_compute_total_rooms') - # FIXING rename to default rooms ? - max_real_rooms = fields.Integer('Default Max Room Allowed') @api.depends('room_ids') def _compute_total_rooms(self): for record in self: - count = 0 - count += len(record.room_ids) # Rooms linked directly - # room_categories = r.room_type_ids.mapped('room_ids.id') - # count += self.env['hotel.room'].search_count([ - # ('categ_id.id', 'in', room_categories) - # ]) # Rooms linked through room type - record.total_rooms_count = count + record.total_rooms_count = len(record.room_ids) def _check_duplicated_rooms(self): # FIXME Using a Many2one relationship duplicated should not been possible pass - @api.constrains('max_real_rooms', 'room_ids') - def _check_max_rooms(self): - warning_msg = "" - # for r in self: - if self.max_real_rooms > self.total_rooms_count: - warning_msg += _('The Maxime rooms allowed can not be greate \ - than total rooms count') - raise models.ValidationError(warning_msg) - @api.multi def get_capacity(self): - # WARNING use selg.capacity directly ? - pass - # self.ensure_one() - # hotel_room_obj = self.env['hotel.room'] - # room_categories = self.room_type_ids.mapped('room_ids.id') - # room_ids = self.room_ids + hotel_room_obj.search([ - # ('categ_id.id', 'in', room_categories) - # ]) - # capacities = room_ids.mapped('capacity') - # return any(capacities) and min(capacities) or 0 + """ + Get the minimum capacity in the rooms of this type or zero if has no rooms + @param self: The object pointer + @return: An integer with the capacity of this room type + """ + self.ensure_one() + capacities = self.room_ids.mapped('capacity') + return any(capacities) and min(capacities) or 0 @api.model - def check_availability_virtual_room(self, checkin, checkout, + # TODO Rename to check_availability_room_type + def check_availability_virtual_room(self, dfrom, dto, room_type_id=False, notthis=[]): """ Check the avalability for an specific type of room + @param self: The object pointer + @param dfrom: Range date from + @param dto: Range date to + @param room_type_id: Room Type + @param notthis: Array excluding Room Types @return: A recordset of free rooms ? """ - occupied = self.env['hotel.reservation'].occupied(checkin, checkout) - rooms_occupied = occupied.mapped('product_id.id') + reservations = self.env['hotel.reservation'].get_reservations(dfrom, dto) + reservations_rooms = reservations.mapped('room_id.id') free_rooms = self.env['hotel.room'].search([ - ('product_id.id', 'not in', rooms_occupied), + ('id', 'not in', reservations_rooms), ('id', 'not in', notthis) ]) if room_type_id: - # hotel_room_obj = self.env['hotel.room'] room_type_id = self.env['hotel.room.type'].search([ ('id', '=', room_type_id) ]) - # room_categories = virtual_room.room_type_ids.mapped('room_ids.id') - # rooms_linked = virtual_room.room_ids | hotel_room_obj.search([ - # ('categ_id.id', 'in', room_categories)]) - # rooms_linked = room_type_id.room_ids + # QUESTION What linked represent? Rooms in this type ? rooms_linked = self.room_ids free_rooms = free_rooms & rooms_linked return free_rooms.sorted(key=lambda r: r.sequence) @@ -135,7 +114,5 @@ class HotelRoomType(models.Model): @api.multi def unlink(self): for record in self: - # Set fixed price to rooms with price from this virtual rooms - # Remove product.product record.product_id.unlink() return super().unlink() diff --git a/hotel/models/inherit_account_payment.py b/hotel/models/inherit_account_payment.py index d111cdc6d..687ba0db7 100644 --- a/hotel/models/inherit_account_payment.py +++ b/hotel/models/inherit_account_payment.py @@ -90,5 +90,5 @@ class AccountPayment(models.Model): raise except_orm(_('Warning'), _('This pay is related with \ more than one Reservation.')) else: - fol.compute_invoices_amount() + fol.compute_amount() return res diff --git a/hotel/models/inherit_payment_return.py b/hotel/models/inherit_payment_return.py index b19d47260..d4188b7c7 100644 --- a/hotel/models/inherit_payment_return.py +++ b/hotel/models/inherit_payment_return.py @@ -36,4 +36,4 @@ class PaymentReturn(models.Model): payments = self.env['account.payment'].search([('move_line_ids','in',line.move_line_ids.ids)]) folio_ids += payments.mapped('folio_id.id') folios = self.env['hotel.folio'].browse(folio_ids) - folios.compute_invoices_amount() + folios.compute_amount() diff --git a/hotel/models/virtual_room.py b/hotel/models/virtual_room.py deleted file mode 100644 index 2ccfa9a99..000000000 --- a/hotel/models/virtual_room.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2017 Solucións Aloxa S.L. -# Dario Lodeiros <> -# Alexandre Díaz -# -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -############################################################################## -from decimal import Decimal -from datetime import datetime, timedelta -import dateutil.parser -# For Python 3.0 and later -from urllib.request import urlopen -import time -from openerp.exceptions import except_orm, UserError, ValidationError -from openerp.tools import ( - misc, - DEFAULT_SERVER_DATE_FORMAT, - DEFAULT_SERVER_DATETIME_FORMAT) -from openerp import models, fields, api, _ -from odoo.addons.hotel import date_utils - - -class VirtualRoom(models.Model): - _name = 'hotel.virtual.room' - _inherits = {'product.product': 'product_id'} - - @api.depends('room_ids', 'room_type_ids') - def _compute_total_rooms(self): - for r in self: - count = 0 - count += len(r.room_ids) # Rooms linked directly - room_categories = r.room_type_ids.mapped('room_ids.id') - count += self.env['hotel.room'].search_count([ - ('categ_id.id', 'in', room_categories) - ]) # Rooms linked through room type - r.total_rooms_count = count - - @api.constrains('room_ids', 'room_type_ids') - def _check_duplicated_rooms(self): - warning_msg = "" - for r in self: - room_categories = self.room_type_ids.mapped('room_ids.id') - if self.room_ids & self.env['hotel.room'].search([ - ('categ_id.id', 'in', room_categories)]): - room_ids = self.room_ids & self.env['hotel.room'].search([ - ('categ_id.id', 'in', room_categories) - ]) - rooms_name = ','.join(str(x.name) for x in room_ids) - warning_msg += _('You can not enter the same room in duplicate \ - (check the room types) %s') % rooms_name - raise models.ValidationError(warning_msg) - - @api.constrains('max_real_rooms', 'room_ids', 'room_type_ids') - def _check_max_rooms(self): - warning_msg = "" - for r in self: - if self.max_real_rooms > self.total_rooms_count: - warning_msg += _('The Maxime rooms allowed can not be greate \ - than total rooms count') - raise models.ValidationError(warning_msg) - - virtual_code = fields.Char('Code') # not used - room_ids = fields.Many2many('hotel.room', string='Rooms') - room_type_ids = fields.Many2many('hotel.room.type', string='Room Types') - total_rooms_count = fields.Integer(compute='_compute_total_rooms') - product_id = fields.Many2one('product.product', 'Product_id', - required=True, delegate=True, - ondelete='cascade') - # FIXME services are related to real rooms - service_ids = fields.Many2many('hotel.services', - string='Included Services') - max_real_rooms = fields.Integer('Default Max Room Allowed') - product_id = fields.Many2one( - 'product.product', required=True, - ondelete='cascade') - active = fields.Boolean(default=True, help="The active field allows you to hide the category without removing it.") - - @api.multi - def get_capacity(self): - self.ensure_one() - hotel_room_obj = self.env['hotel.room'] - room_categories = self.room_type_ids.mapped('room_ids.id') - room_ids = self.room_ids + hotel_room_obj.search([ - ('categ_id.id', 'in', room_categories) - ]) - capacities = room_ids.mapped('capacity') - return any(capacities) and min(capacities) or 0 - - @api.model - def check_availability_virtual_room(self, checkin, checkout, - virtual_room_id=False, notthis=[]): - occupied = self.env['hotel.reservation'].occupied(checkin, checkout) - rooms_occupied = occupied.mapped('product_id.id') - free_rooms = self.env['hotel.room'].search([ - ('product_id.id', 'not in', rooms_occupied), - ('id', 'not in', notthis) - ]) - if virtual_room_id: - hotel_room_obj = self.env['hotel.room'] - virtual_room = self.env['hotel.virtual.room'].search([ - ('id', '=', virtual_room_id) - ]) - room_categories = virtual_room.room_type_ids.mapped('room_ids.id') - rooms_linked = virtual_room.room_ids | hotel_room_obj.search([ - ('categ_id.id', 'in', room_categories)]) - free_rooms = free_rooms & rooms_linked - return free_rooms.sorted(key=lambda r: r.sequence) - - @api.multi - def unlink(self): - for record in self: - # Set fixed price to rooms with price from this virtual rooms - rooms = self.env['hotel.room'].search([ - ('sale_price_type', '=', 'vroom'), - ('price_virtual_room', '=', record.id) - ]) - for room in rooms: - room.sale_price_type = 'fixed' - # Remove product.product - record.product_id.unlink() - return super(VirtualRoom, self).unlink() diff --git a/hotel/views/hotel_folio.xml b/hotel/views/hotel_folio.xml index 3ac843ffb..52f05db24 100644 --- a/hotel/views/hotel_folio.xml +++ b/hotel/views/hotel_folio.xml @@ -84,10 +84,10 @@ id="payment_smart_button" icon="fa-money" name="action_pay" - attrs="{'invisible': ['|',('invoices_amount','<=',0)]}"> + attrs="{'invisible': ['|',('pending_amount','<=',0)]}">
- Pending Payment @@ -221,8 +221,8 @@ - - + +
+ + - + - - - - - + + @@ -263,7 +265,7 @@ - + @@ -413,10 +415,10 @@ /> - @@ -75,7 +74,6 @@ - - - - @@ -55,7 +47,6 @@ - @@ -65,7 +56,6 @@ Room Type hotel.room.type form - tree,form