trytonpsk-hotel/booking.py

829 lines
29 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from __future__ import with_statement
from datetime import datetime, timedelta
from decimal import Decimal
from trytond.model import Workflow, ModelView, ModelSQL, fields
from trytond.wizard import (
Wizard, StateView, Button, StateTransition, StateReport
)
from trytond.report import Report
from trytond.pyson import Eval, If, In, Get, Not, Or, Equal, Bool
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.exceptions import UserError
from trytond.i18n import gettext
from constants import (
STATE_BOOKING, REGISTRATION_STATE, SEGMENT, GUARANTEE, SATISFACTION,
MEDIA, PLAN, SOURCE, INVOICE_METHOD, COMPLEMENTARY,
)
STATES = {
'readonly': Eval('state') != 'offer',
}
STATES_CONFIRMED = {
'readonly': Eval('state') != 'offer',
'required': Eval('state') == 'confirmed',
}
STATES_CHECKIN = {
'readonly': Eval('registration_state').in_(
['check_in', 'no_show', 'cancelled']
),
'required': Eval('registration_state') == 'check_in',
}
_ZERO = Decimal('0.0')
class Booking(Workflow, ModelSQL, ModelView):
'Booking'
__name__ = 'hotel.booking'
_rec_name = 'number'
number = fields.Char('Number', readonly=True, select=True,
help="Sequence of reservation.")
party = fields.Many2One('party.party', 'Party', required=False,
select=True, help="Person or company owner of the booking.",
states={
'required': Eval('state') == 'check_in',
'readonly': Not(In(Eval('state'), ['offer', 'confirmed'])),
})
contact = fields.Char('Contact', states=STATES_CHECKIN)
payment_term = fields.Many2One('account.invoice.payment_term',
'Payment Term', states=STATES_CHECKIN)
booking_date = fields.DateTime('Booking Date', readonly=False, states=STATES)
person_num = fields.Integer('Person Number', states=STATES)
group = fields.Boolean('Group', states=STATES)
complementary = fields.Boolean('Complementary', states=STATES)
type_complementary = fields.Selection(COMPLEMENTARY, 'Type Complementary', states={
'invisible': ~Bool(Eval('complementary')),
'required': Bool(Eval('complementary')),
})
party_seller = fields.Many2One('hotel.channel', 'Channel',
states=STATES_CHECKIN, help="Agency or channel that do reservation.")
state = fields.Selection(STATE_BOOKING, 'State', readonly=True, required=True)
registration_state = fields.Selection(REGISTRATION_STATE, 'State Registration',
readonly=True, depends=['lines'])
state_string = state.translated('state')
price_list = fields.Many2One('product.price_list', 'Price List',
states={
'readonly': Or(Not(Equal(Eval('state'), 'offer')),
Bool(Eval('lines'))),
}, depends=['state', 'company', 'lines'])
company = fields.Many2One('company.company', 'Company', required=True,
states=STATES, domain=[('id', If(In('company',
Eval('context', {})), '=', '!='), Get(Eval('context', {}),
'company', 0))], readonly=True)
lines = fields.One2Many('hotel.folio', 'booking', 'Lines',
states={
'required': Eval('state') == 'confirmed',
'readonly': Eval('registration_state').in_(['check_in', 'check_out']),
}, depends=['state', 'party'], context={'party': Eval('party')})
cancellation_policy = fields.Many2One('hotel.policy.cancellation',
'Cancellation Policy', states=STATES_CHECKIN)
currency = fields.Many2One('currency.currency', 'Currency',
required=True, states={
'readonly': (Eval('state') != 'offer') |
(Eval('lines', [0]) & Eval('currency', 0)),
}, depends=['state'])
invoice_method = fields.Selection(INVOICE_METHOD, 'Invoice Method',
required=False, states=STATES_CONFIRMED)
satisfaction = fields.Selection(SATISFACTION, 'Satisfaction')
media = fields.Selection(MEDIA, 'Media', states=STATES_CHECKIN,
help="Media from booking coming from.")
media_string = media.translated('media')
plan = fields.Selection(PLAN, 'Commercial Plan', states=STATES_CHECKIN,
help="Plans offered by hotel and selected by guest for booking.")
plan_string = plan.translated('plan')
source_contact = fields.Selection(SOURCE, 'Source Contact',
states=STATES_CHECKIN,
help="Advertising source that create booking opportunity by guest.")
source_contact_string = source_contact.translated('source_contact')
comments = fields.Text('Comments', states=STATES_CHECKIN)
segment = fields.Selection(SEGMENT, 'Tourism Segment',
states=STATES_CHECKIN)
segment_string = segment.translated('segment')
guarantee = fields.Selection(GUARANTEE, 'Guarantee',
states=STATES_CHECKIN)
guarantee_string = guarantee.translated('guarantee')
untaxed_amount = fields.Function(fields.Numeric('Untaxed Amount',
digits=(16, 2), depends=['lines']), 'get_untaxed_amount')
tax_amount = fields.Function(fields.Numeric('Tax Amount',
digits=(16, 2), depends=['lines']), 'get_tax_amount')
total_amount = fields.Function(fields.Numeric('Total Amount',
digits=(16, 2), depends=['lines']), 'get_total_amount')
code = fields.Char('Code', states={'readonly': True})
booker = fields.Many2One('party.party', 'Booker', states={'readonly': True})
created_channel = fields.DateTime('Created Channel', states={'readonly': True})
vouchers = fields.Many2Many('hotel.booking-account.voucher', 'booking',
'voucher', 'Vouchers', states=STATES_CHECKIN, domain=[
], depends=['party'])
vip = fields.Boolean('V.I.P. Customer', states=STATES)
vehicles_num = fields.Integer('Vehicles Number', states=STATES,
help="Number of vehicles that bring with guests.")
vehicle_plate = fields.Integer('Vehicle Plate', states=STATES)
travel_cause = fields.Char('Travel Cause', states=STATES)
taxes_exception = fields.Boolean('Taxes Exception', states=STATES)
total_advance = fields.Function(fields.Numeric('Total Advance',
digits=(16, 2)), 'get_total_advance')
pending_to_pay = fields.Function(fields.Numeric('Pending to Pay',
digits=(16, 2)),'get_pending_to_pay')
breakfast_included = fields.Boolean('Breakfast Included')
@classmethod
def __setup__(cls):
super(Booking, cls).__setup__()
cls._order.insert(0, ('create_date', 'DESC'))
cls._transitions |= set((
('offer', 'confirmed'),
('offer', 'cancelled'),
('confirmed', 'offer'),
('cancelled', 'offer'),
('confirmed', 'cancelled'),
('confirmed', 'not_show'),
))
cls._buttons.update({
'select_rooms': {
'invisible': Eval('state').in_(['finished', 'cancelled', 'not_show']),
},
'create_guest': {
'invisible': Eval('state') != 'offer',
},
'cancel': {
'invisible': Eval('state').in_(
['cancelled', 'not_show', 'finished'])
},
'offer': {
'invisible': Eval('state').in_(['offer', 'confirmed'])
},
'confirm': {
'invisible': ~Eval('state').in_(['offer', 'not_show'])
},
'not_show': {
'invisible': Eval('state') != 'confirmed',
},
'pay_advance': {
'invisible': Eval('state').in_(['finished', 'cancelled']),
},
'send_email': {
'invisible': Eval('state') != 'confirmed'
},
})
@classmethod
def trigger_create(cls, records):
cls.set_number(records)
@classmethod
def copy(cls, bookings, default=None):
if default is None:
default = {}
default = default.copy()
default['number'] = None
default['state'] = 'offer'
default['booking_date'] = datetime.now()
return super(Booking, cls).copy(bookings, default=default)
@staticmethod
def default_currency():
Company = Pool().get('company.company')
company = Transaction().context.get('company')
if company:
return Company(company).currency.id
@staticmethod
def default_company():
return Transaction().context.get('company') or False
@staticmethod
def default_state():
return 'offer'
@staticmethod
def default_invoice_method():
return 'by_booking'
@staticmethod
def default_booking_date():
now = datetime.now()
return now
def get_person_num(self, name):
res = 0
for line in self.lines:
if line.num_adult:
res += line.num_adult
if line.num_children:
res += line.num_children
return res
@fields.depends('party', 'price_list')
def on_change_party(self):
if self.party and self.party.sale_price_list:
self.price_list = self.party.sale_price_list.id
self.price_list.rec_name = self.party.sale_price_list.rec_name
@fields.depends('party_seller')
def on_change_party_seller(self):
if self.party_seller and self.party_seller.price_list:
self.price_list = self.party_seller.price_list
@classmethod
@ModelView.button_action('hotel.wizard_select_rooms')
def select_rooms(cls, bookings):
pass
@classmethod
@ModelView.button_action('hotel.wizard_party_guest')
def create_guest(cls, bookings):
pass
@classmethod
@ModelView.button_action('hotel.wizard_booking_advance_voucher')
def pay_advance(cls, bookings):
pass
@classmethod
@ModelView.button
@Workflow.transition('offer')
def offer(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('not_show')
def not_show(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('confirmed')
def confirm(cls, records):
cls.set_number(records)
for rec in records:
# FIXME check if does not exist previous occupancy if exist update state
rec.update_folio('pending')
@classmethod
@ModelView.button
def no_show(cls, records):
for record in records:
cls.write([record], {'registration_state': 'no_show'})
record.cancel_occupancy()
@classmethod
@ModelView.button
def send_email(cls, records):
for reserve in records:
if reserve.state == 'confirmed':
reserve.send_email_to()
@classmethod
def set_number(cls, bookings):
"""
Fill the number field with the booking sequence
"""
pool = Pool()
Config = pool.get('hotel.configuration')
config = Config.get_configuration()
for booking in bookings:
if booking.number or not config.booking_sequence:
continue
number = config.booking_sequence.get()
cls.write([booking], {'number': number})
def update_folio(self, state):
Line = Pool().get('hotel.folio')
Line.write(list(self.lines), {'registration_state': state})
def check_rooms(self):
pool = Pool()
rooms_ids = []
for line in self.lines:
rooms_ids.append(line.room.id)
Housekeeping = pool.get('hotel.housekeeping')
housekeepings = Housekeeping.search([
('state', '!=', 'clean')
])
for hk in housekeepings:
if hk.room.id in rooms_ids:
raise UserError(gettext('hotel.msg_room_no_clean', s=hk.room.name))
def get_context_price(self, product):
context = {}
if getattr(self, 'currency', None):
context['currency'] = self.currency.id
if getattr(self, 'party', None):
context['customer'] = self.party.id
if getattr(self, 'booking_date', None):
context['sale_date'] = self.booking_date
if getattr(self, 'price_list', None):
context['price_list'] = self.price_list.id
context['uom'] = product.template.default_uom.id
# Set taxes before unit_price to have taxes in context of sale price
taxes = []
pattern = {}
if not self.taxes_exception:
for tax in product.customer_taxes_used:
if self.party and self.party.customer_tax_rule:
tax_ids = self.party.customer_tax_rule.apply(tax, pattern)
if tax_ids:
taxes.extend(tax_ids)
continue
taxes.append(tax.id)
if self.party and self.party.customer_tax_rule:
tax_ids = self.party.customer_tax_rule.apply(None, pattern)
if tax_ids:
taxes.extend(tax_ids)
context['taxes'] = taxes
return context
def get_total_advance(self, name):
Advance = Pool().get('hotel.booking-account.voucher')
vouchers = Advance.search([('booking', '=', self.id)])
res = sum([voucher.voucher.amount_to_pay for voucher in vouchers])
return res
def get_pending_to_pay(self, name):
if self.total_amount:
return self.total_amount - (self.total_advance or 0)
def get_total_amount(self, name):
res = 0
if self.tax_amount or self.untaxed_amount:
res = self.tax_amount + self.untaxed_amount
return res
def get_taxes(self, product):
ctx = self.get_context_price(product)
return ctx['taxes']
def get_tax_amount(self, name):
Tax = Pool().get('account.tax')
res = _ZERO
for line in self.lines:
taxes_ids = self.get_taxes(line.product)
if taxes_ids:
taxes = Tax.browse(taxes_ids)
tax_list = Tax.compute(
taxes, line.unit_price or _ZERO, line.nights_quantity or 0
)
tax_amount = sum([t['amount'] for t in tax_list], _ZERO)
res += tax_amount
res = Decimal(round(res, 2))
return res
def get_untaxed_amount(self, name):
res = _ZERO
for line in self.lines:
res += line.total_amount
return res
def get_occupancies(self, name):
occupancies = set()
for line in self.lines:
if line.occupancy_line:
occupancies.add(line.occupancy_line.id)
return list(occupancies)
def send_email_to(self):
pool = Pool()
config = pool.get('hotel.configuration')(1)
Template = pool.get('email.template')
email = self.party.email
if email:
Template.send(config.booking_email_template, self, email)
else:
raise UserError(gettext('El cliente no tiene un correo asociado'))
@fields.depends('price_list', 'breakfast_included')
def on_change_price_list(self):
if self.price_list:
self.breakfast_included = self.price_list.breakfast_included
class BookingReport(Report):
__name__ = 'hotel.booking'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
user = Pool().get('res.user')(Transaction().user)
report_context['company'] = user.company
return report_context
class SelectRoomsAsk(ModelView):
'Seelct Rooms Assistant'
__name__ = 'hotel.booking.select_rooms.ask'
arrival_date = fields.Date('Arrival Date', required=True)
departure_date = fields.Date('Departure Date', required=True)
accommodation = fields.Many2One('product.product',
'Accommodation', domain=[
('template.kind', '=', 'accommodation'),
])
rooms = fields.Many2Many('hotel.room', None, None,
'Rooms', domain=[
('id', 'in', Eval('targets')),
])
overbooking = fields.Boolean('Overbooking')
targets = fields.Function(fields.Many2Many('hotel.room', None, None,
'Targets'), 'on_change_with_targets')
@staticmethod
def default_accommodation():
Configuration = Pool().get('hotel.configuration')
config = Configuration.get_configuration()
if config.default_accommodation:
return config.default_accommodation.id
@fields.depends('arrival_date', 'departure_date', 'accommodation', 'overbooking')
def on_change_with_targets(self, name=None):
pool = Pool()
RoomTemplate = pool.get('hotel.room-product.template')
Room = pool.get('hotel.room')
Folio = pool.get('hotel.folio')
res = []
if not self.accommodation or not self.arrival_date or not self.departure_date:
return res
if self.overbooking:
return [r.id for r in Room.search([])]
room_templates = RoomTemplate.search([
('template.accommodation_capacity', '>=', self.accommodation.accommodation_capacity)
])
rooms_ids = [t.room.id for t in room_templates]
rooms_available_ids = Folio.get_available_rooms(
self.arrival_date,
self.departure_date,
rooms_ids=rooms_ids
)
return rooms_available_ids
class SelectRooms(Wizard):
'Select Rooms'
__name__ = 'hotel.booking.select_rooms'
"""
this is the wizard that allows the front desk employee to select
rooms, based on the requirements listed by the customer.
"""
start = StateView('hotel.booking.select_rooms.ask',
'hotel.view_select_rooms_form', [
Button('Exit', 'end', 'tryton-cancel'),
Button('Add and Continue', 'add_continue', 'tryton-forward'),
Button('Add', 'add_rooms', 'tryton-ok'),
]
)
add_rooms = StateTransition()
add_continue = StateTransition()
def transition_add_rooms(self):
self._add_rooms()
return 'end'
def transition_add_continue(self):
self._add_rooms()
return 'start'
def _add_rooms(self):
pool = Pool()
Line = pool.get('hotel.folio')
Booking = pool.get('hotel.booking')
booking = Booking(Transaction().context.get('active_id'))
lines_to_create = []
ctx = {}
if booking.price_list:
ctx['price_list'] = booking.price_list
ctx['sale_date'] = self.start.arrival_date
ctx['currency'] = booking.currency.id
if booking.party:
ctx['customer'] = booking.party.id
product = self.start.accommodation
quantity = (self.start.departure_date - self.start.arrival_date).days
unit_price = product.template.list_price
if booking.price_list:
with Transaction().set_context(ctx):
unit_price = booking.price_list.compute(
booking.party, product, unit_price,
quantity, product.default_uom)
unit_price = booking.currency.round(unit_price)
for room in self.start.rooms:
values = {
'booking': booking.id,
'product': product.id,
'room': room.id,
'arrival_date': self.start.arrival_date,
'departure_date': self.start.departure_date,
'unit_price': unit_price,
}
if booking.party:
values['main_guest'] = booking.party.id
values.update({'product': product.id})
lines_to_create.append(values)
Line.create(lines_to_create)
booking.save()
class BookingVoucher(ModelSQL):
'Booking - Voucher'
__name__ = 'hotel.booking-account.voucher'
_table = 'booking_vouchers_rel'
booking = fields.Many2One('hotel.booking', 'Booking',
ondelete='CASCADE', select=True, required=True)
voucher = fields.Many2One('account.voucher', 'Voucher', select=True,
domain=[('voucher_type', '=', 'receipt')], ondelete='RESTRICT',
required=True)
@classmethod
def set_voucher_origin(cls, voucher_id, booking_id):
cls.create([{
'voucher': voucher_id,
'booking': booking_id,
}])
class BookingForecastStart(ModelView):
'Booking Forecast Start'
__name__ = 'hotel.print_booking_forecast.start'
date = fields.Date('Start Date', required=True)
company = fields.Many2One('company.company', 'Company', required=True)
@staticmethod
def default_date():
Date_ = Pool().get('ir.date')
return Date_.today()
@staticmethod
def default_company():
return Transaction().context.get('company')
class BookingForecast(Wizard):
'Booking Forecast'
__name__ = 'hotel.print_booking_forecast'
start = StateView('hotel.print_booking_forecast.start',
'hotel.print_booking_forecast_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-print', default=True),
])
print_ = StateReport('hotel.booking_forecast.report')
def do_print_(self, action):
company = self.start.company
data = {
'date': self.start.date,
'company': company.id,
}
return action, data
def transition_print_(self):
return 'end'
class BookingForecastReport(Report):
__name__ = 'hotel.booking_forecast.report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
MAX_DAYS = 30
pool = Pool()
Company = pool.get('company.company')
Room = pool.get('hotel.room')
BookingFolio = pool.get('hotel.folio')
rooms = Room.search([])
alldays = {}
alldays_convert = {}
for nd in range(MAX_DAYS):
day_n = 'day' + str((nd + 1))
tdate = data['date'] + timedelta(nd)
data[day_n] = tdate
data['total_' + day_n] = 0
data[('revenue_' + day_n)] = 0
data[('rate_' + day_n)] = 0
alldays[day_n] = ''
alldays_convert[tdate] = day_n
date_init = data['date']
date_limit = data['date'] + timedelta(MAX_DAYS)
dom = [['OR', [
('arrival_date', '<=', date_init),
('departure_date', '>=', date_init),
], [
('arrival_date', '>=', date_init),
('arrival_date', '<=', date_limit),
],
], ('booking.state', 'not in', ['no_show', 'cancelled'])]
lines = BookingFolio.search(dom)
drooms = {}
for room in rooms:
drooms[room.id] = {'name': room.name}
drooms[room.id].update(alldays.copy())
for line in lines:
_delta = (line.departure_date - line.arrival_date).days
for i in range(_delta):
dt = line.arrival_date + timedelta(i)
if dt >= date_init and dt < date_limit \
and dt >= data['date']:
dayn = alldays_convert[dt]
drooms[line.room.id][dayn] = "X"
data['total_' + dayn] += 1
data['revenue_' + dayn] += float(line.unit_price) / 1000000
for i in range(MAX_DAYS):
day_n = 'day' + str((i + 1))
data['rate_' + day_n] = (data['total_' + day_n] * 100.0) / len(rooms)
report_context['records'] = list(drooms.values())
report_context['date'] = data['date']
report_context['company'] = Company(data['company']).party.name
return report_context
class RoomsOccupancyStart(ModelView):
'Rooms Occupancy Start'
__name__ = 'hotel.print_rooms_occupancy.start'
date = fields.Date('Date', required=True)
company = fields.Many2One('company.company', 'Company', required=True)
@staticmethod
def default_date():
Date_ = Pool().get('ir.date')
return Date_.today()
@staticmethod
def default_company():
return Transaction().context.get('company')
class RoomsOccupancy(Wizard):
'Rooms Occupancy'
__name__ = 'hotel.print_rooms_occupancy'
start = StateView('hotel.print_rooms_occupancy.start',
'hotel.print_rooms_occupancy_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Open', 'print_', 'tryton-print', default=True),
])
print_ = StateReport('hotel.rooms_occupancy.report')
def do_print_(self, action):
company = self.start.company
data = {
'date': self.start.date,
'company': company.id,
}
return action, data
def transition_print_(self):
return 'end'
class RoomsOccupancyReport(Report):
__name__ = 'hotel.rooms_occupancy.report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
Company = pool.get('company.company')
Room = pool.get('hotel.room')
Folio = pool.get('hotel.folio')
start_date = data['date']
all_rooms = Room.search([], order=[('code', 'ASC')])
operations = Folio.search([
('start_date', '<=', start_date),
('state', '=', 'open'),
('kind', '=', 'occupancy'),
])
def _get_default_room(r):
res = {
'room': r.name,
'guest': None,
'num_guest': None,
'party': None,
'arrival': None,
'departure': None,
'booking': None,
'reference': None,
'amount': None,
'balance': None,
'state': None,
}
return res
rooms_map = {room.id: _get_default_room(room) for room in all_rooms}
occupancy_rooms = 0
for op in operations:
name = op.main_guest.name if op.main_guest else op.party.name
rooms_map[op.room.id].update({
'guest': name,
'num_guest': len(op.guests),
'party': op.party.name,
'arrival': op.start_date,
'departure': op.end_date,
'reference': op.reference,
'amount': op.total_amount,
'balance': op.total_amount_today,
'state': op.state,
})
occupancy_rooms += 1
if all_rooms:
occupancy_rate = (float(len(operations)) / len(all_rooms)) * 100
else:
occupancy_rate = 0
user = Pool().get('res.user')(Transaction().user)
recs = rooms_map.values()
report_context['records'] = rooms_map
report_context['occupancy_rate'] = occupancy_rate
report_context['occupancy_rooms'] = occupancy_rooms
report_context['company'] = Company(data['company']).party.name
report_context['date'] = data['date']
report_context['user'] = user.rec_name
return report_context
class BookingDailyStart(ModelView):
'Booking Daily Start'
__name__ = 'hotel.print_booking_daily.start'
date = fields.Date('Date', required=True)
company = fields.Many2One('company.company', 'Company', required=True)
@staticmethod
def default_date():
Date_ = Pool().get('ir.date')
return Date_.today()
@staticmethod
def default_company():
return Transaction().context.get('company')
class BookingDaily(Wizard):
'Rooms Occupancy'
__name__ = 'hotel.booking_daily'
start = StateView('hotel.print_booking_daily.start',
'hotel.print_booking_daily_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Open', 'print_', 'tryton-print', default=True),
])
print_ = StateReport('hotel.booking_daily.report')
def do_print_(self, action):
company = self.start.company
data = {
'date': self.start.date,
'company': company.id,
}
return action, data
def transition_print_(self):
return 'end'
class BookingDailyReport(Report):
__name__ = 'hotel.booking_daily.report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
Company = pool.get('company.company')
Folio = pool.get('hotel.folio')
records = Folio.search([
('arrival_date', '=', data['date']),
], order=[('room.code', 'ASC')])
report_context['records'] = records
report_context['company'] = Company(data['company']).party.name
report_context['date'] = data['date']
return report_context