mirror of
https://bitbucket.org/presik/trytonpsk-hotel.git
synced 2023-12-14 07:52:52 +01:00
2978 lines
106 KiB
Python
2978 lines
106 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 datetime import datetime, timedelta, date
|
|
from decimal import Decimal
|
|
import copy
|
|
|
|
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, Bool
|
|
from trytond.transaction import Transaction
|
|
from trytond.pool import Pool
|
|
from trytond.exceptions import UserError
|
|
from trytond.i18n import gettext
|
|
from trytond.modules.account.tax import TaxableMixin
|
|
from .constants import (
|
|
STATE_BOOKING, REASON, GUARANTEE, SATISFACTION, MEDIA, PLAN,
|
|
COMPLEMENTARY, PAYMENT_METHOD_CHANNEL, TYPE_DOCUMENT
|
|
)
|
|
|
|
SEPARATOR = ' | '
|
|
_ZERO = Decimal('0.0')
|
|
|
|
STATES = {
|
|
'readonly': Eval('state') != 'offer',
|
|
}
|
|
|
|
NO_CHANGES = ['finished', 'cancelled', 'not_show']
|
|
|
|
STATES_BLOCKED = {
|
|
'readonly': Eval('state').in_(NO_CHANGES),
|
|
}
|
|
|
|
STATES_CONFIRMED = {
|
|
'readonly': Eval('state') != 'offer',
|
|
'required': Eval('state') == 'confirmed',
|
|
}
|
|
|
|
|
|
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', 'Customer', required=False,
|
|
select=True, states=STATES_BLOCKED,
|
|
help="Person or company owner of the booking.")
|
|
contact = fields.Char('Contact', states=STATES_BLOCKED,
|
|
help='Main contact or person how request booking')
|
|
payment_term = fields.Many2One('account.invoice.payment_term',
|
|
'Payment Term', states=STATES_BLOCKED)
|
|
booking_date = fields.DateTime('Booking Date', readonly=True)
|
|
guests_num = fields.Function(fields.Integer('Person Number'),
|
|
'get_person_num')
|
|
adult_num = fields.Function(fields.Integer('Adult Number'),
|
|
'get_person_num')
|
|
children_num = fields.Function(fields.Integer('Children Number'),
|
|
'get_person_num')
|
|
group = fields.Boolean('Group', states=STATES_BLOCKED)
|
|
corporative = fields.Boolean('Corporative', states=STATES_BLOCKED)
|
|
complementary = fields.Boolean('Complementary', states=STATES_BLOCKED)
|
|
type_complementary = fields.Selection(COMPLEMENTARY, 'Type Complementary',
|
|
states=STATES_BLOCKED)
|
|
channel = fields.Many2One('hotel.channel', 'Channel',
|
|
states={
|
|
'invisible': Eval('media') != 'ota',
|
|
'readonly': Eval('state').in_(['finished', 'cancelled']),
|
|
},
|
|
help="Agency or channel that do reservation.")
|
|
state = fields.Selection(STATE_BOOKING, 'State', readonly=True,
|
|
required=True)
|
|
state_string = state.translated('state')
|
|
price_list = fields.Many2One('product.price_list', 'Price List',
|
|
states={
|
|
'readonly': 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)
|
|
# 'required': Eval('state') == 'confirmed',
|
|
lines = fields.One2Many('hotel.folio', 'booking', 'Folios',
|
|
states={
|
|
'readonly': Eval('state').in_(['finished', 'cancelled']),
|
|
}, depends=['state', 'party'], context={'party': Eval('party')})
|
|
cancellation_policy = fields.Many2One('hotel.policy.cancellation',
|
|
'Cancellation Policy', states=STATES_BLOCKED)
|
|
currency = fields.Many2One('currency.currency', 'Currency',
|
|
required=True, states={
|
|
'readonly': (Eval('state') != 'offer') |
|
|
(Eval('lines', [0]) & Eval('currency', 0)),
|
|
}, depends=['state'])
|
|
satisfaction = fields.Selection(SATISFACTION, 'Satisfaction')
|
|
media = fields.Selection(MEDIA, 'Media', states=STATES_BLOCKED,
|
|
help="Way through which the booking arrives.")
|
|
media_string = media.translated('media')
|
|
plan = fields.Selection(PLAN, 'Commercial Plan', states=STATES_BLOCKED,
|
|
help="Plans offered by hotel and selected by guest for booking.")
|
|
plan_string = plan.translated('plan')
|
|
comments = fields.Text('Comments', states=STATES_BLOCKED)
|
|
reason = fields.Selection(REASON, 'Tourism Segment', states=STATES_BLOCKED)
|
|
reason_string = reason.translated('segment')
|
|
guarantee = fields.Selection(GUARANTEE, 'Guarantee', states=STATES_BLOCKED)
|
|
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')
|
|
collect_amount = fields.Function(fields.Numeric('Collect Amount',
|
|
digits=(16, 2), depends=['lines']), 'get_collect_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={'readonly': False})
|
|
ota_booking_code = fields.Char('OTA Code', select=True,
|
|
states={'invisible': Eval('media') != 'ota'}
|
|
)
|
|
payments = fields.One2Many('account.statement.line', 'source',
|
|
'Payments', states=STATES_BLOCKED, readonly=True)
|
|
vehicles_num = fields.Integer('Vehicles Number', states=STATES_BLOCKED,
|
|
help="Number of vehicles that bring with guests.")
|
|
travel_cause = fields.Char('Travel Cause', states=STATES_BLOCKED)
|
|
taxes_exception = fields.Boolean('Taxes Exception', states=STATES_BLOCKED)
|
|
total_advances = fields.Function(fields.Numeric('Total Advance',
|
|
digits=(16, 2)), 'get_total_advances')
|
|
pending_to_pay = fields.Function(fields.Numeric('Pending to Pay',
|
|
digits=(16, 2)), 'get_pending_to_pay')
|
|
breakfast_included = fields.Boolean('Breakfast Included',
|
|
states=STATES_BLOCKED)
|
|
channel_commission = fields.Function(fields.Numeric('Channel Commission',
|
|
digits=(16, 2), depends=['lines']), 'get_channel_commission')
|
|
commission = fields.Many2One('commission', 'Commission')
|
|
channel_invoice = fields.Many2One('account.invoice', 'Channel Invoice',
|
|
states={
|
|
'invisible': ~Eval('channel'),
|
|
'readonly': True
|
|
})
|
|
channel_payment_method = fields.Selection(PAYMENT_METHOD_CHANNEL,
|
|
'Channel Payment Method', states={'invisible': ~Eval('channel')},
|
|
depends=['channel']
|
|
)
|
|
invoices = fields.Function(fields.Many2Many('account.invoice',
|
|
None, None, 'Invoices'), 'get_invoices')
|
|
extra_commissions = fields.Many2Many('hotel.booking-channel.commission',
|
|
'booking', 'commission', 'Channel Commission', domain=[
|
|
('channel', '=', Eval('channel'))
|
|
], states=STATES_BLOCKED)
|
|
stock_moves = fields.Function(fields.One2Many('stock.move', 'origin', 'Moves',
|
|
readonly=True), 'get_stock_moves')
|
|
channel_icon = fields.Function(fields.Char('Channel Icon'),
|
|
'get_channel_icon')
|
|
emails = fields.One2Many('email.activity', 'origin', 'Emails',
|
|
readonly=True)
|
|
responsible_payment = fields.Selection([
|
|
('holder', 'Holder'),
|
|
('holder_guest', 'Holder / Guest'),
|
|
('guest', 'Guest'),
|
|
], 'Responsible for Payment', states={
|
|
'required': True,
|
|
'readonly': Eval('state').in_(NO_CHANGES),
|
|
})
|
|
credit_card = fields.Char('Credit Card', states=STATES_BLOCKED)
|
|
vip = fields.Boolean('V.I.P. Customer', states=STATES_BLOCKED)
|
|
pending_acco = fields.Function(fields.Numeric('Pending to Pay',
|
|
digits=(16, 2)), 'get_pending_to_pay')
|
|
pending_charges = fields.Function(fields.Numeric('Pending to Pay',
|
|
digits=(16, 2)), 'get_pending_to_pay')
|
|
income_moves = fields.One2Many('account.move', 'origin', 'Income Moves',
|
|
states={
|
|
'readonly': Eval('state').in_(['finished', 'cancelled']),
|
|
})
|
|
space = fields.Many2One('analytic_account.space', 'Space',
|
|
states=STATES_BLOCKED)
|
|
collection_mode = fields.Selection([
|
|
('', ''),
|
|
('anticipated', 'Anticipated'),
|
|
('post_checkin', 'Post Checkin'),
|
|
], 'Collection Mode', required=False,
|
|
help="Commission collection mode")
|
|
link_web_checkin = fields.Function(fields.Char('Link Web Check-in'),
|
|
'get_link_web_checkin')
|
|
|
|
@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'),
|
|
('confirmed', 'finished'),
|
|
('not_show', 'confirmed'),
|
|
))
|
|
cls._buttons.update({
|
|
'select_rooms': {
|
|
'invisible': Eval('state').in_(NO_CHANGES),
|
|
},
|
|
'update_holder': {
|
|
'invisible': Eval('state').in_(NO_CHANGES),
|
|
},
|
|
'cancel': {
|
|
'invisible': Eval('state').in_(NO_CHANGES)
|
|
},
|
|
'offer': {
|
|
'invisible': Eval('state').in_(['offer', 'finished'])
|
|
},
|
|
'confirm': {
|
|
'invisible': ~Eval('state').in_(['offer', 'not_show'])
|
|
},
|
|
'not_show': {
|
|
'invisible': Eval('state') != 'confirmed',
|
|
},
|
|
'do_payment': {
|
|
'invisible': Eval('state').in_(NO_CHANGES),
|
|
},
|
|
'send_email_booking': {
|
|
'invisible': Eval('state').in_(['finished'])
|
|
},
|
|
'send_email_checkin': {
|
|
'invisible': Eval('state').in_(['finished'])
|
|
},
|
|
'bill': {
|
|
'invisible': ~Eval('state').in_(['confirmed', 'not_show']),
|
|
},
|
|
})
|
|
|
|
@staticmethod
|
|
def default_responsible_payment():
|
|
return 'holder'
|
|
|
|
@staticmethod
|
|
def default_collection_mode():
|
|
return 'post_checkin'
|
|
|
|
@staticmethod
|
|
def default_space():
|
|
Config = Pool().get('hotel.configuration')
|
|
config = Config.get_configuration()
|
|
if config and config.space_booking:
|
|
return config.space_booking.id
|
|
|
|
@staticmethod
|
|
def default_payment_term():
|
|
Config = Pool().get('hotel.configuration')
|
|
config = Config.get_configuration()
|
|
if config.payment_term:
|
|
return config.payment_term.id
|
|
|
|
def get_channel_icon(self, name):
|
|
name_icon = 'hotel-channel-house'
|
|
if self.channel and self.channel.code:
|
|
name_icon = f'hotel-channel-{self.channel.code}'
|
|
return name_icon
|
|
|
|
@classmethod
|
|
def trigger_create(cls, records):
|
|
cls.set_number(records)
|
|
|
|
@classmethod
|
|
def delete(cls, records):
|
|
for record in records:
|
|
if record.number:
|
|
raise UserError(gettext('hotel.msg_can_no_delete_booking'))
|
|
super(Booking, cls).delete(records)
|
|
|
|
@classmethod
|
|
def copy(cls, bookings, default=None):
|
|
if default is None:
|
|
default = {}
|
|
default = default.copy()
|
|
default['number'] = None
|
|
default['booking_date'] = datetime.now()
|
|
default['payments'] = None
|
|
return super(Booking, cls).copy(bookings, default=default)
|
|
|
|
def get_stock_moves(self, name=None):
|
|
moves = []
|
|
for folio in self.lines:
|
|
for charge in folio.charges:
|
|
if charge.move:
|
|
moves.append(charge.move.id)
|
|
return moves
|
|
|
|
@classmethod
|
|
def search_rec_name(cls, name, clause):
|
|
_, operator, value = clause
|
|
if operator.startswith('!') or operator.startswith('not '):
|
|
bool_op = 'AND'
|
|
else:
|
|
bool_op = 'OR'
|
|
domain = [
|
|
bool_op,
|
|
('number', operator, value),
|
|
('ota_booking_code', operator, value),
|
|
('party.name', operator, value),
|
|
('contact', operator, value),
|
|
]
|
|
return domain
|
|
|
|
@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_plan():
|
|
return 'bed_breakfast'
|
|
|
|
@staticmethod
|
|
def default_booking_date():
|
|
now = datetime.now()
|
|
return now
|
|
|
|
def _round_taxes(self, taxes):
|
|
if not self.currency:
|
|
return
|
|
for taxline in taxes.values():
|
|
taxline['amount'] = self.currency.round(taxline['amount'])
|
|
|
|
def _get_taxes(self):
|
|
pool = Pool()
|
|
Tax = pool.get('account.tax')
|
|
Configuration = pool.get('account.configuration')
|
|
|
|
taxes = {}
|
|
with Transaction().set_context({}):
|
|
config = Configuration(1)
|
|
tax_rounding = config.get_multivalue('tax_rounding')
|
|
|
|
def compute(_taxes, unit_price, quantity):
|
|
l_taxes = Tax.compute(list(_taxes), unit_price, quantity)
|
|
for tax in l_taxes:
|
|
taxline = TaxableMixin._compute_tax_line(**tax)
|
|
# Base must always be rounded per folio as there will
|
|
# be one tax folio per taxable_lines
|
|
if self.currency:
|
|
taxline['base'] = self.currency.round(taxline['base'])
|
|
if taxline not in taxes:
|
|
taxes[taxline] = taxline
|
|
else:
|
|
taxes[taxline]['base'] += taxline['base']
|
|
taxes[taxline]['amount'] += taxline['amount']
|
|
# if tax_rounding == 'line':
|
|
# self._round_taxes(taxes)
|
|
|
|
for folio in self.lines:
|
|
_taxes = folio.product.customer_taxes_used
|
|
if folio.registration_state in ('check_in', 'check_out'):
|
|
for occ in folio.occupancy:
|
|
if not occ.charge:
|
|
compute(_taxes, occ.unit_price, 1)
|
|
|
|
for charge in folio.charges:
|
|
compute(charge.taxes, charge.unit_price, charge.quantity)
|
|
else:
|
|
compute(_taxes, folio.unit_price, folio.nights_quantity)
|
|
|
|
return taxes
|
|
|
|
def get_invoices(self, name=None):
|
|
res = []
|
|
for folio in self.lines:
|
|
if folio.invoice_line:
|
|
res.append(folio.invoice_line.invoice.id)
|
|
for charge in folio.charges:
|
|
if charge.invoice_line:
|
|
res.append(charge.invoice_line.invoice.id)
|
|
return list(set(res))
|
|
|
|
def get_person_num(self, name):
|
|
res = {
|
|
'guests_num': [],
|
|
'adult_num': [],
|
|
'children_num': [],
|
|
}
|
|
|
|
for line in self.lines:
|
|
res['guests_num'].append(line.num_adults + line.num_children)
|
|
if name == 'adult_num' and line.num_adults:
|
|
res[name].append(line.num_adults)
|
|
elif name == 'children_num' and line.num_children:
|
|
res[name].append(line.num_children)
|
|
return sum(res[name])
|
|
|
|
@fields.depends('ota_booking_code', 'lines')
|
|
def on_change_ota_booking_code(self):
|
|
if self.ota_booking_code:
|
|
for line in self.lines:
|
|
line.reference = self.ota_booking_code
|
|
|
|
def is_person(self):
|
|
if self.party.type_document in (
|
|
'12', '13', '21', '22', '41', '42', '47'):
|
|
return True
|
|
|
|
@fields.depends('party', 'price_list', 'lines')
|
|
def on_change_party(self):
|
|
if self.party:
|
|
if 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
|
|
for folio in self.lines:
|
|
if self.is_person():
|
|
folio.main_guest = self.party.id
|
|
|
|
@fields.depends('channel')
|
|
def on_change_channel(self):
|
|
if self.channel:
|
|
if self.channel.invoice_to == 'channel':
|
|
self.party = self.channel.agent.party.id
|
|
self.responsible_payment = 'holder_guest'
|
|
if self.channel.payment_method == 'ota_collect':
|
|
self.responsible_payment = 'holder_guest'
|
|
|
|
self.channel_payment_method = self.channel.payment_method
|
|
self.price_list = self.channel.price_list
|
|
self.collection_mode = self.channel.collection_mode
|
|
else:
|
|
self.channel_payment_method = None
|
|
self.price_list = None
|
|
self.collection_mode = None
|
|
|
|
@classmethod
|
|
@ModelView.button_action('hotel.wizard_select_rooms')
|
|
def select_rooms(cls, records):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button_action('hotel.wizard_update_holder')
|
|
def update_holder(cls, records):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button_action('hotel.wizard_statement_payment_form')
|
|
def do_payment(cls, records):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('offer')
|
|
def offer(cls, records):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancelled')
|
|
def cancel(cls, records):
|
|
for rec in records:
|
|
for folio in rec.lines:
|
|
if folio.registration_state in ['check_in', 'check_out']:
|
|
raise UserError(gettext('hotel.msg_no_delete_folios'))
|
|
else:
|
|
folio.delete([folio])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('not_show')
|
|
def not_show(cls, records):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('confirmed')
|
|
def confirm(cls, records):
|
|
for rec in records:
|
|
rec.update_folio('pending')
|
|
rec.state = 'confirmed'
|
|
rec.save()
|
|
rec.send_email('booking_email')
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def no_show(cls, records):
|
|
for record in records:
|
|
cls.write([record], {'state': 'no_show'})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def send_email_booking(cls, records):
|
|
for booking in records:
|
|
if booking.state == 'confirmed':
|
|
booking.send_email('booking_email')
|
|
|
|
@classmethod
|
|
@ModelView.button_action('hotel.wizard_bill_booking')
|
|
def bill(cls, records):
|
|
pass
|
|
|
|
@classmethod
|
|
def create_channel_voucher(cls, bk):
|
|
Voucher = Pool().get('account.voucher')
|
|
# If ota_collect is paymode for folios
|
|
if not bk.channel or bk.responsible_payment != 'ota_collect':
|
|
return
|
|
|
|
payment_mode = bk.channel.payment_mode
|
|
if not payment_mode:
|
|
raise UserError(gettext('hotel.msg_missing_payment_mode'))
|
|
|
|
party = bk.channel.agent.party
|
|
account_id = Voucher.get_account('receipt', payment_mode)
|
|
|
|
amount = bk.collect_amount - bk.channel_commission
|
|
lines = [{
|
|
'detail': f'OTA Code {bk.ota_booking_code}',
|
|
'amount': amount,
|
|
'amount_original': amount,
|
|
'account': party.account_receivable.id,
|
|
}]
|
|
|
|
to_create = {
|
|
'party': party.id,
|
|
'voucher_type': 'receipt',
|
|
'date': date.today(),
|
|
'description': f'Booking {bk.number}',
|
|
'payment_mode': payment_mode.id,
|
|
'account': account_id,
|
|
'journal': payment_mode.journal.id,
|
|
'lines': [('create', lines)],
|
|
'amount_to_pay': amount,
|
|
'method_counterpart': 'one_line',
|
|
# 'state': 'draft',
|
|
}
|
|
voucher, = Voucher.create([to_create])
|
|
Voucher.process([voucher])
|
|
cls.write([bk], {'vouchers': [('add', [voucher])]})
|
|
|
|
@classmethod
|
|
def concile_charges(cls, records):
|
|
'''
|
|
We need mark charges pending to pay as paid, if the customer
|
|
already paid all booking charges
|
|
'''
|
|
for booking in records:
|
|
if booking.pending_to_pay != 0:
|
|
continue
|
|
for folio in booking.lines:
|
|
for charge in folio.charges:
|
|
if charge.status == 'pending':
|
|
charge.status = 'paid'
|
|
charge.save()
|
|
|
|
@classmethod
|
|
def check_finished(cls, records):
|
|
for rec in records:
|
|
_folios, _charges = cls.pending_to_invoice(rec.lines)
|
|
if not _folios and not _charges:
|
|
cls.write([rec], {'state': 'finished'})
|
|
|
|
@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})
|
|
|
|
@classmethod
|
|
def reconcile(cls, booking, voucher):
|
|
pool = Pool()
|
|
VoucherConfig = pool.get('account.voucher_configuration')
|
|
MoveLine = pool.get('account.move.line')
|
|
invoice = None
|
|
config = VoucherConfig.get_configuration()
|
|
account = config.customer_advance_account
|
|
for folio in booking.lines:
|
|
invoice = folio.invoice
|
|
if invoice and invoice.state == 'paid':
|
|
continue
|
|
advances = []
|
|
payments = []
|
|
for voucher in booking.vouchers:
|
|
for line in voucher.move.lines:
|
|
if line.account.id == account.id and not line.reconciliation:
|
|
advances.append(voucher)
|
|
break
|
|
elif invoice and line.account.id == invoice.account.id:
|
|
payments.append(line)
|
|
|
|
if invoice:
|
|
to_reconcile_lines = []
|
|
if advances:
|
|
invoice.create_move_advance(advances)
|
|
invoice.save()
|
|
if payments:
|
|
invoice.add_payment_lines({invoice: payments})
|
|
invoice.save()
|
|
if invoice.amount_to_pay == Decimal(0):
|
|
for ml in invoice.payment_lines:
|
|
if not ml.reconciliation:
|
|
to_reconcile_lines.append(ml)
|
|
for ml in invoice.move.lines:
|
|
if not ml.reconciliation and ml.account.id == invoice.account.id:
|
|
to_reconcile_lines.append(ml)
|
|
if to_reconcile_lines:
|
|
MoveLine.reconcile(to_reconcile_lines)
|
|
|
|
@classmethod
|
|
def _get_charge_line(cls, cha, bk):
|
|
description = cha.product.template.name
|
|
if cha.order:
|
|
description.append(cha.order)
|
|
if cha.kind == 'accommodation':
|
|
description = cha.description + ' | ' + str(cha.date_service)
|
|
|
|
return {
|
|
'description': description,
|
|
'quantity': cha.quantity,
|
|
'product': cha.product,
|
|
'unit_price': bk.currency.round(cha.unit_price),
|
|
'charge': cha,
|
|
'origin': str(cha.folio),
|
|
'taxes': cha.taxes,
|
|
'taxes_exception': bk.taxes_exception,
|
|
'analytic_account': cha.analytic_account,
|
|
}
|
|
|
|
@classmethod
|
|
def _get_accommodation_line(cls, fo, bk):
|
|
return {
|
|
'folios': [fo],
|
|
'description': fo.get_room_info(),
|
|
'quantity': fo.nights_quantity,
|
|
'product': fo.product,
|
|
'unit_price': fo.unit_price,
|
|
'taxes': fo.taxes,
|
|
'origin': str(fo),
|
|
'taxes_exception': bk.taxes_exception,
|
|
}
|
|
|
|
@classmethod
|
|
def add_invoice_charges(cls, kind, charge, res):
|
|
pass
|
|
|
|
@classmethod
|
|
def get_grouped_invoices(cls, folios, kind=None, party=None):
|
|
res = {}
|
|
for folio in folios:
|
|
bk = folio.booking
|
|
price_list = bk.price_list
|
|
agent_id = bk.channel.agent.id if bk.channel else None
|
|
for ch in folio.charges:
|
|
if ch.invoice_line:
|
|
continue
|
|
if kind == 'only_products' and ch.kind == 'accommodation':
|
|
continue
|
|
if kind == 'only_accommodation' and ch.kind == 'product':
|
|
continue
|
|
|
|
if bk.responsible_payment == 'holder':
|
|
party = bk.party
|
|
elif bk.responsible_payment == 'guest':
|
|
party = folio.main_guest
|
|
else:
|
|
if ch.kind == 'accommodation':
|
|
party = bk.party
|
|
else:
|
|
party = folio.main_guest
|
|
|
|
if party.id not in res.keys():
|
|
res[party.id] = {
|
|
'party': party,
|
|
'currency': bk.currency.id,
|
|
'payment_term': bk.payment_term.id,
|
|
'number': bk.number,
|
|
'reference': [folio.registration_card],
|
|
'rooms': [folio.room.name],
|
|
'price_list': price_list.id if price_list else None,
|
|
'company': bk.company.id,
|
|
'lines': [],
|
|
}
|
|
if ch.kind == 'accommodation' and agent_id:
|
|
res[party.id]['agent'] = agent_id
|
|
res[party.id]['ota_booking_code'] = bk.ota_booking_code or ''
|
|
res[party.id]['guests_qty'] = len(folio.guests)
|
|
line = cls._get_charge_line(ch, bk)
|
|
res[party.id]['lines'].append(line)
|
|
return res
|
|
|
|
@classmethod
|
|
def _get_invoice_line(cls, invoice, line, record=None):
|
|
product = line['product']
|
|
new_line = {
|
|
'type': 'line',
|
|
'invoice': invoice.id,
|
|
'unit': product.template.default_uom.id,
|
|
'account': product.template.account_category.account_revenue_used.id,
|
|
'invoice_type': 'out',
|
|
'quantity': line['quantity'],
|
|
'unit_price': line['unit_price'],
|
|
'product': product.id,
|
|
'party': invoice.party.id,
|
|
'description': line['description'],
|
|
'taxes': [('add', line['taxes'])],
|
|
'origin': line['origin'],
|
|
}
|
|
analytic_account = line.get('analytic_account', None)
|
|
if analytic_account:
|
|
new_line['analytic_accounts'] = [('create', [{
|
|
'account': analytic_account.id,
|
|
'root': analytic_account.root.id,
|
|
}])]
|
|
return new_line
|
|
|
|
def do_moves(self):
|
|
Move = Pool().get('stock.move')
|
|
moves = {}
|
|
to_create = []
|
|
for folio in self.lines:
|
|
for charge in folio.charges:
|
|
move = self.get_move(charge)
|
|
if move:
|
|
moves[folio.id] = move
|
|
for m in moves:
|
|
moves[m].state = 'draft'
|
|
to_create.append(moves[m]._save_values)
|
|
|
|
Move.create(to_create)
|
|
Move.do(self.moves)
|
|
|
|
def get_move(self, charge):
|
|
'''
|
|
Return stock move for charge according a storage
|
|
'''
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
product = self.product
|
|
if not product or product.type != 'goods' and self.quantity <= 0:
|
|
return
|
|
|
|
if not charge.folio.storage:
|
|
return None
|
|
customer_location = self.party.customer_location
|
|
move = Move()
|
|
move.quantity = self.quantity
|
|
move.uom = product.default_uom
|
|
move.product = product
|
|
# Add on create charge take default storage
|
|
# from folio if isn not present
|
|
move.from_location = charge.folio.storage
|
|
move.to_location = customer_location
|
|
move.state = 'draft'
|
|
move.company = self.company
|
|
move.unit_price = self.unit_price
|
|
move.currency = self.company.currency
|
|
move.planned_date = self.date_service
|
|
move.origin = charge
|
|
return move
|
|
|
|
@classmethod
|
|
def get_taxes(cls, product, party=None, currency=None):
|
|
ctx = cls.get_context_price(product, party, currency)
|
|
return ctx['taxes']
|
|
|
|
@classmethod
|
|
def pending_to_invoice(cls, folios):
|
|
_folios = []
|
|
_charges = []
|
|
for folio in folios:
|
|
if not folio.invoice_line or not folio.to_invoice:
|
|
_folios.append(folio)
|
|
for charge in folio.charges:
|
|
if not charge.invoice_line and charge.to_invoice:
|
|
_charges.append(charge)
|
|
return _folios, _charges
|
|
|
|
@classmethod
|
|
def create_channel_pay(cls, bk):
|
|
pool = Pool()
|
|
Note = pool.get('account.note')
|
|
NoteLine = pool.get('account.note.line')
|
|
Invoice = pool.get('account.invoice')
|
|
Config = pool.get('account.voucher_configuration')
|
|
config = Config.get_configuration()
|
|
commission = bk.currency.round(bk.channel_commission)
|
|
note, = Note.create([{
|
|
'description': bk.number,
|
|
'journal': config.default_journal_note.id,
|
|
'date': date.today(),
|
|
'state': 'draft',
|
|
}])
|
|
line1, = NoteLine.create([{
|
|
'note': note.id,
|
|
'debit': 0,
|
|
'credit': commission,
|
|
'party': bk.channel.agent.party.id,
|
|
'account': bk.channel_invoice.account.id,
|
|
'description': bk.ota_booking_code,
|
|
}])
|
|
line2, = NoteLine.create([{
|
|
'note': note.id,
|
|
'debit': commission,
|
|
'credit': 0,
|
|
'party': bk.channel.agent.party.id,
|
|
'account': bk.channel.debit_account.id,
|
|
'description': bk.number,
|
|
}])
|
|
Note.write([note], {'lines': [('add', [line1, line2])]})
|
|
Note.post([note])
|
|
lines_to_add = []
|
|
for line in note.move.lines:
|
|
if line.account.id == bk.channel_invoice.account.id:
|
|
lines_to_add.append(line.id)
|
|
Invoice.write([bk.channel_invoice], {
|
|
'payment_lines': [('add', lines_to_add)],
|
|
})
|
|
bk.channel_invoice.save()
|
|
|
|
@classmethod
|
|
def create_channel_invoice(cls, bk):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
InvoiceLine = pool.get('account.invoice.line')
|
|
Folio = pool.get('hotel.folio')
|
|
if not bk.channel:
|
|
return
|
|
data = {
|
|
'party': bk.channel.agent.party,
|
|
'reference': bk.number,
|
|
'description': f"{bk.ota_booking_code} | {bk.party.name}",
|
|
'payment_term': bk.payment_term,
|
|
'number': bk.number,
|
|
}
|
|
invoice = cls._get_new_invoice(data)
|
|
invoice.on_change_invoice_type()
|
|
invoice.save()
|
|
for folio in bk.lines:
|
|
if folio.invoice_line:
|
|
continue
|
|
_folio = {
|
|
'product': folio.product,
|
|
'quantity': folio.nights_quantity,
|
|
'unit_price': folio.unit_price,
|
|
'description': folio.room.name,
|
|
'origin': str(folio),
|
|
'taxes_exception': bk.taxes_exception,
|
|
}
|
|
line, = InvoiceLine.create([
|
|
cls._get_invoice_line(invoice, _folio)
|
|
])
|
|
Folio.write([folio], {'invoice_line': line.id})
|
|
invoice.save()
|
|
invoice.update_taxes([invoice])
|
|
cls.write([bk], {'channel_invoice': invoice.id})
|
|
Invoice.validate([invoice])
|
|
invoice.save()
|
|
invoice, = Invoice.browse([invoice.id])
|
|
try:
|
|
Invoice.submit([invoice])
|
|
invoice.save()
|
|
except Exception as e:
|
|
print(e)
|
|
try:
|
|
invoice = Invoice(invoice.id)
|
|
Invoice.post([invoice])
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
@classmethod
|
|
def create_invoice(cls, folios, kind=None, party=None):
|
|
pool = Pool()
|
|
for folio in folios:
|
|
folio.add_charge_occupancy(ctx='invoice')
|
|
FolioCharge = pool.get('hotel.folio.charge')
|
|
InvoiceLine = pool.get('account.invoice.line')
|
|
invoice = {}
|
|
|
|
invoice_to_create = cls.get_grouped_invoices(folios, kind, party)
|
|
if not invoice_to_create:
|
|
return
|
|
|
|
for rec in invoice_to_create.values():
|
|
invoice = cls._get_new_invoice(rec)
|
|
invoice.on_change_invoice_type()
|
|
invoice.save()
|
|
|
|
for _line in rec['lines']:
|
|
line, = InvoiceLine.create([
|
|
cls._get_invoice_line(invoice, _line)
|
|
])
|
|
FolioCharge.write([_line.get('charge')], {
|
|
'invoice_line': line.id,
|
|
})
|
|
invoice.save()
|
|
invoice.update_taxes([invoice])
|
|
|
|
def get_link_web_checkin(self, name=None):
|
|
transaction = Transaction().context
|
|
pool = Pool()
|
|
host, _ = transaction['_request']['http_host'].split(":")
|
|
db = pool.database_name
|
|
link = f'http://{host}:3000/app/{db}/web_checkin/{self.id}'
|
|
return link
|
|
|
|
@classmethod
|
|
def _get_new_invoice(cls, data):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
Agent = pool.get('commission.agent')
|
|
Journal = pool.get('account.journal')
|
|
PaymentTerm = pool.get('account.invoice.payment_term')
|
|
Date = pool.get('ir.date')
|
|
date_ = Date.today()
|
|
party = data['party']
|
|
|
|
ota_code = data.get('ota_booking_code', '')
|
|
description = []
|
|
if data.get('description'):
|
|
description.extend(data['description'])
|
|
else:
|
|
description.append(data['number'])
|
|
if ota_code:
|
|
description.append(ota_code)
|
|
if data.get('rooms'):
|
|
description.extend(data.get('rooms'))
|
|
description = SEPARATOR.join(description)
|
|
reference = data.get('reference', '')
|
|
if reference:
|
|
reference = SEPARATOR.join(reference)
|
|
agent = None
|
|
if data.get('agent'):
|
|
agent = Agent(data['agent'])
|
|
|
|
journal, = Journal.search([('type', '=', 'revenue')], limit=1)
|
|
|
|
address = party.address_get(type='invoice')
|
|
payment_term_id = data.get('payment_term', None)
|
|
if not payment_term_id:
|
|
payment_terms = PaymentTerm.search([])
|
|
payment_term_id = payment_terms[0].id
|
|
|
|
return Invoice(
|
|
company=data['company'],
|
|
payment_term=payment_term_id,
|
|
party=party.id,
|
|
account=party.account_receivable_used.id,
|
|
invoice_date=date_,
|
|
description=description,
|
|
state='draft',
|
|
reference=reference,
|
|
agent=agent,
|
|
journal=journal,
|
|
type='out',
|
|
invoice_type='1',
|
|
invoice_address=address.id,
|
|
)
|
|
|
|
def update_folio(self, state):
|
|
Folio = Pool().get('hotel.folio')
|
|
Folio.write(list(self.lines), {'registration_state': state})
|
|
if state == 'pending':
|
|
for folio in self.lines:
|
|
folio.add_occupancy()
|
|
|
|
@classmethod
|
|
def add_charges_occupancy(cls, bookings):
|
|
for booking in bookings:
|
|
for folio in booking.lines:
|
|
folio.add_charge_occupancy()
|
|
|
|
@classmethod
|
|
def do_revenue_accounting(cls, bookings):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
Move = pool.get('account.move')
|
|
MoveLine = pool.get('account.move.line')
|
|
Journal = pool.get('account.journal')
|
|
Period = pool.get('account.period')
|
|
Config = pool.get('hotel.configuration')
|
|
journal, = Journal.search([
|
|
('type', '=', 'revenue'),
|
|
], limit=1)
|
|
config = Config.get_configuration()
|
|
today = Date.today()
|
|
for bk in bookings:
|
|
period_id = Period.find(bk.company.id, date=today)
|
|
_move, = Move.create([{
|
|
'date': today,
|
|
'journal': journal.id,
|
|
'period': period_id,
|
|
'origin': str(bk),
|
|
'state': 'draft',
|
|
'description': '',
|
|
}])
|
|
debit = 0
|
|
for folio in bk.lines:
|
|
reference = folio.registration_card
|
|
for charge in folio.charges:
|
|
if charge.line_move:
|
|
continue
|
|
product = charge.product
|
|
account_id = product.account_revenue_used.id
|
|
credit = charge.amount
|
|
debit += credit
|
|
desc = f'{product.rec_name} | {str(charge.date_service)}'
|
|
_line = {
|
|
'description': desc,
|
|
'party': bk.party.id,
|
|
'debit': 0,
|
|
'credit': credit,
|
|
'reference': reference,
|
|
'account': account_id,
|
|
'move': _move.id
|
|
}
|
|
mline, = MoveLine.create([_line])
|
|
charge.line_move = mline.id
|
|
charge.save()
|
|
if not config.recognise_account:
|
|
raise UserError(gettext('hotel.msg_missing_recognise_account'))
|
|
recognise_account_id = config.recognise_account.id
|
|
move_line, = MoveLine.create([{
|
|
'description': bk.number,
|
|
'party': bk.party.id,
|
|
'debit': debit,
|
|
'credit': 0,
|
|
'reference': reference,
|
|
'account': recognise_account_id,
|
|
'move': _move.id
|
|
}])
|
|
charge.line_move = move_line.id
|
|
charge.save()
|
|
Move.post([_move])
|
|
|
|
@classmethod
|
|
def get_context_price(
|
|
cls, product, party=None, currency=None,
|
|
_date=None, price_list=None, taxes_exception=False
|
|
):
|
|
context = {}
|
|
if currency:
|
|
context['currency'] = currency.id
|
|
if party:
|
|
context['customer'] = party.id
|
|
if _date:
|
|
context['sale_date'] = _date
|
|
if price_list:
|
|
context['price_list'] = 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 taxes_exception and party:
|
|
for tax in product.customer_taxes_used:
|
|
if party and party.customer_tax_rule:
|
|
tax_ids = party.customer_tax_rule.apply(tax, pattern)
|
|
if tax_ids:
|
|
taxes.extend(tax_ids)
|
|
continue
|
|
taxes.append(tax.id)
|
|
if party and party.customer_tax_rule:
|
|
tax_ids = party.customer_tax_rule.apply(None, pattern)
|
|
if tax_ids:
|
|
taxes.extend(tax_ids)
|
|
|
|
context['taxes'] = taxes
|
|
return context
|
|
|
|
def get_total_advances(self, name):
|
|
Advance = Pool().get('hotel.booking-account.voucher')
|
|
advances = Advance.search([('booking', '=', self.id)])
|
|
vouchers = sum([ad.voucher.amount_to_pay for ad in advances])
|
|
payments = sum([pay.amount for pay in self.payments])
|
|
folios_payments = sum([fol.total_advances for fol in self.lines])
|
|
return self.currency.round(vouchers + payments + folios_payments)
|
|
|
|
def get_pending_to_pay(self, name):
|
|
res = 0
|
|
if name == 'pending_to_pay':
|
|
if self.total_amount:
|
|
res = self.total_amount - (self.total_advances or 0)
|
|
if res > -1 and res < 1:
|
|
res = 0
|
|
elif name == 'pending_acco':
|
|
res = sum(fol.pending_accommodation for fol in self.lines)
|
|
elif name == 'pending_charges':
|
|
res = sum(fol.pending_charges for fol in self.lines)
|
|
return self.currency.round(res)
|
|
|
|
def get_collect_amount(self, name):
|
|
res = sum(folio.room_amount for folio in self.lines)
|
|
return self.currency.round(res)
|
|
|
|
def get_total_amount(self, name):
|
|
res = 0
|
|
if self.tax_amount or self.untaxed_amount:
|
|
res = self.tax_amount + self.untaxed_amount
|
|
return self.currency.round(res)
|
|
|
|
def get_tax_amount(self, name):
|
|
taxes_computed = self._get_taxes()
|
|
res = sum([t['amount'] for t in taxes_computed], _ZERO)
|
|
return self.currency.round(res)
|
|
|
|
def get_untaxed_amount(self, name):
|
|
res = []
|
|
for folio in self.lines:
|
|
if folio.registration_state in ('check_in', 'check_out'):
|
|
for occ in folio.occupancy:
|
|
if not occ.charge:
|
|
res.append(occ.unit_price)
|
|
for charge in folio.charges:
|
|
res.append(charge.amount)
|
|
else:
|
|
res.append(folio.room_amount)
|
|
return self.currency.round(sum(res))
|
|
|
|
def get_channel_commission(self, name):
|
|
res = [
|
|
line.commission_amount for line in self.lines
|
|
if line.commission_amount]
|
|
base_comm = [folio.on_change_with_room_amount() for folio in self.lines]
|
|
for comm in self.extra_commissions:
|
|
extras = sum(base_comm) * Decimal(comm.commission / 100)
|
|
res.append(extras)
|
|
return self.currency.round(sum(res))
|
|
|
|
def send_email(self, name):
|
|
pool = Pool()
|
|
config = pool.get('hotel.configuration')(1)
|
|
Template = pool.get('email.template')
|
|
Activity = pool.get('email.activity')
|
|
email = self.party.email
|
|
template = getattr(config, name)
|
|
if not template:
|
|
return
|
|
template.subject = f'{template.subject} No. {self.number}'
|
|
if email:
|
|
Template.send(template, self, email, attach=True)
|
|
Activity.create([{
|
|
'template': template.id,
|
|
'origin': str(self),
|
|
'status': 'sended',
|
|
}])
|
|
else:
|
|
raise UserError(gettext('hotel.msg_missing_party_email'))
|
|
|
|
@fields.depends('price_list', 'breakfast_included')
|
|
def on_change_price_list(self):
|
|
if self.price_list:
|
|
self.breakfast_included = self.price_list.breakfast_included
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def send_email_checkin(cls, records):
|
|
for booking in records:
|
|
if booking.state == 'confirmed':
|
|
booking.send_email('check_in_email')
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def send_email_customer_experience(cls, records):
|
|
for booking in records:
|
|
if booking.state == 'confirmed':
|
|
booking.send_email('customer_experience')
|
|
|
|
|
|
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 BookingStatementReport(Report):
|
|
__name__ = 'hotel.booking_statement'
|
|
|
|
@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):
|
|
'Select 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')
|
|
unit_price = fields.Numeric('Unit Price', digits=(16, 2), required=True)
|
|
|
|
@fields.depends('accommodation', 'departure_date', 'arrival_date')
|
|
def on_change_with_unit_price(self):
|
|
Booking = Pool().get('hotel.booking')
|
|
booking = Booking(Transaction().context.get('active_id'))
|
|
ctx = {}
|
|
if booking.price_list:
|
|
ctx['price_list'] = booking.price_list
|
|
ctx['sale_date'] = self.arrival_date
|
|
ctx['currency'] = booking.currency.id
|
|
if booking.party:
|
|
ctx['customer'] = booking.party.id
|
|
if self.accommodation and self.departure_date and self.arrival_date:
|
|
product = self.accommodation
|
|
unit_price = product.template.list_price
|
|
quantity = (self.departure_date - self.arrival_date).days
|
|
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)
|
|
return unit_price
|
|
|
|
@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()
|
|
Folio = pool.get('hotel.folio')
|
|
Booking = pool.get('hotel.booking')
|
|
booking = Booking(Transaction().context.get('active_id'))
|
|
lines_to_create = []
|
|
product = self.start.accommodation
|
|
|
|
for room in self.start.rooms:
|
|
values = {
|
|
'booking': booking.id,
|
|
'product': product.id,
|
|
'reference': booking.ota_booking_code,
|
|
'contact': booking.contact,
|
|
'room': room.id,
|
|
'arrival_date': self.start.arrival_date,
|
|
'departure_date': self.start.departure_date,
|
|
'unit_price': self.start.unit_price,
|
|
}
|
|
if booking.party and booking.is_person():
|
|
values['main_guest'] = booking.party.id
|
|
values.update({'product': product.id})
|
|
lines_to_create.append(values)
|
|
Folio.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 BookingStatementLine(ModelSQL):
|
|
'Booking - Statement Line'
|
|
__name__ = 'hotel.booking-statement.line'
|
|
_table = 'hotel_booking_statement_line_rel'
|
|
booking = fields.Many2One('hotel.booking', 'Booking',
|
|
ondelete='CASCADE', select=True, required=True)
|
|
statement_line = fields.Many2One('account.statement.line',
|
|
'Statement Line', ondelete='CASCADE', required=True)
|
|
|
|
|
|
class RevenueForecastStart(ModelView):
|
|
'Revenue Forecast Start'
|
|
__name__ = 'hotel.print_revenue_forecast.start'
|
|
company = fields.Many2One('company.company', 'Company', required=True)
|
|
fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscalyear',
|
|
required=True)
|
|
period = fields.Many2One('account.period', 'Period', required=True,
|
|
domain=[
|
|
('fiscalyear', '=', Eval('fiscalyear')),
|
|
])
|
|
|
|
@staticmethod
|
|
def default_fiscalyear():
|
|
Fiscalyear = Pool().get('account.fiscalyear')
|
|
fiscalyear, = Fiscalyear.search([],
|
|
limit=1, order=[('start_date', 'DESC')])
|
|
return fiscalyear.id
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
|
|
class RevenueForecast(Wizard):
|
|
'Revenue Forecast'
|
|
__name__ = 'hotel.print_revenue_forecast'
|
|
start = StateView(
|
|
'hotel.print_revenue_forecast.start',
|
|
'hotel.print_revenue_forecast_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Print', 'print_', 'tryton-print', default=True),
|
|
])
|
|
print_ = StateReport('hotel.revenue_forecast.report')
|
|
|
|
def do_print_(self, action):
|
|
company = self.start.company
|
|
data = {
|
|
'period': self.start.period.id,
|
|
'company': company.id,
|
|
}
|
|
return action, data
|
|
|
|
def transition_print_(self):
|
|
return 'end'
|
|
|
|
|
|
class RevenueForecastReport(Report):
|
|
__name__ = 'hotel.revenue_forecast.report'
|
|
|
|
@classmethod
|
|
def get_context(cls, records, header, data):
|
|
report_context = super().get_context(records, header, data)
|
|
pool = Pool()
|
|
Company = pool.get('company.company')
|
|
Period = pool.get('account.period')
|
|
Room = pool.get('hotel.room')
|
|
BookingFolio = pool.get('hotel.folio')
|
|
|
|
period = Period(data['period'])
|
|
start_date = period.start_date
|
|
end_date = period.end_date
|
|
max_day = (end_date - start_date).days
|
|
|
|
rooms = Room.search([])
|
|
alldays = {}
|
|
alldays_convert = {}
|
|
data['revenue'] = 0
|
|
data['occupancy_rate'] = 0
|
|
for nd in range(31):
|
|
day_n = 'day' + str((nd + 1))
|
|
tdate = start_date + timedelta(nd)
|
|
zero = 0
|
|
if nd > max_day:
|
|
tdate = ''
|
|
zero = ''
|
|
data[day_n] = tdate
|
|
data['total_' + day_n] = zero
|
|
data[('revenue_' + day_n)] = zero
|
|
data[('rate_' + day_n)] = zero
|
|
alldays[day_n] = ''
|
|
alldays_convert[tdate] = day_n
|
|
|
|
dom = [
|
|
['OR', [
|
|
('arrival_date', '<=', start_date),
|
|
('departure_date', '>=', start_date),
|
|
], [
|
|
('arrival_date', '>=', start_date),
|
|
('arrival_date', '<=', end_date),
|
|
],
|
|
], ('booking.state', 'not in', ['no_show', 'cancelled'])]
|
|
|
|
lines = BookingFolio.search(dom)
|
|
drooms = {}
|
|
|
|
available_nights = len(rooms) * max_day
|
|
nights = 0
|
|
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 >= start_date and dt < end_date \
|
|
and dt >= start_date:
|
|
revenue_day = float(line.unit_price) / 1000000
|
|
dayn = alldays_convert[dt]
|
|
drooms[line.room.id][dayn] = revenue_day
|
|
data['total_' + dayn] += 1
|
|
data['revenue_' + dayn] += revenue_day
|
|
data['revenue'] += float(line.unit_price)
|
|
nights += 1
|
|
|
|
for i in range(max_day):
|
|
day_n = 'day' + str((i + 1))
|
|
data['rate_' + day_n] = (data['total_' + day_n] * 100.0) / len(rooms)
|
|
|
|
if nights > 0:
|
|
data['occupancy_rate'] = float(nights / available_nights) * 100
|
|
|
|
report_context['records'] = list(drooms.values())
|
|
report_context['period'] = period.name
|
|
report_context['company'] = Company(data['company'])
|
|
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 = {
|
|
'ids': [],
|
|
'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')
|
|
User = pool.get('res.user')
|
|
Room = pool.get('hotel.room')
|
|
Folio = pool.get('hotel.folio')
|
|
start_date = data['date']
|
|
all_rooms = Room.search([], order=[('code', 'ASC')])
|
|
folios = Folio.search([
|
|
('arrival_date', '<=', start_date),
|
|
('departure_date', '>', start_date),
|
|
('registration_state', 'in', ['check_in', 'check_out']),
|
|
])
|
|
|
|
def _get_default_room(r):
|
|
res = {
|
|
'room': r.name,
|
|
'guest': None,
|
|
'num_guest': None,
|
|
'party': None,
|
|
'arrival': None,
|
|
'departure': None,
|
|
'booking': None,
|
|
'registration_card': None,
|
|
'amount': 0,
|
|
'registration_state': None,
|
|
'pending_total': 0,
|
|
}
|
|
return res
|
|
|
|
rooms_map = {room.id: _get_default_room(room) for room in all_rooms}
|
|
occupancy_rooms = 0
|
|
for fo in folios:
|
|
rooms_map[fo.room.id].update({
|
|
'guest': fo.main_guest.name,
|
|
'num_guest': len(fo.guests),
|
|
'party': fo.booking.party.name if fo.booking.party else '',
|
|
'arrival': fo.arrival_date,
|
|
'departure': fo.departure_date,
|
|
'registration_card': fo.registration_card,
|
|
'amount': fo.total_amount,
|
|
'booking': fo.booking.number,
|
|
'registration_state': fo.registration_state_string,
|
|
'pending_total': fo.pending_total,
|
|
})
|
|
occupancy_rooms += 1
|
|
|
|
if all_rooms:
|
|
occupancy_rate = (float(len(folios)) / len(all_rooms)) * 100
|
|
else:
|
|
occupancy_rate = 0
|
|
|
|
report_context['records'] = rooms_map.values()
|
|
report_context['occupancy_rate'] = occupancy_rate
|
|
report_context['occupancy_rooms'] = occupancy_rooms
|
|
report_context['company'] = Company(data['company'])
|
|
report_context['date'] = data['date']
|
|
report_context['user'] = User(Transaction().user).rec_name
|
|
return report_context
|
|
|
|
|
|
class BookingStatusStart(ModelView):
|
|
'Booking Status Start'
|
|
__name__ = 'hotel.print_booking_status.start'
|
|
start_date = fields.Date('Start Date', required=True)
|
|
end_date = fields.Date('End Date', required=True)
|
|
company = fields.Many2One('company.company', 'Company', required=True)
|
|
grouped = fields.Boolean('Grouped')
|
|
state = fields.Selection([
|
|
('offer', 'Offer'),
|
|
('confirmed', 'Confirmed'),
|
|
('cancelled', 'Cancelled'),
|
|
('not_show', 'Not Show'),
|
|
('finished', 'Finished'),
|
|
('', ''),
|
|
], 'State')
|
|
|
|
@staticmethod
|
|
def default_date():
|
|
Date_ = Pool().get('ir.date')
|
|
return Date_.today()
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
|
|
class BookingStatus(Wizard):
|
|
'Booking Status'
|
|
__name__ = 'hotel.booking_status'
|
|
start = StateView(
|
|
'hotel.print_booking_status.start',
|
|
'hotel.print_booking_status_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Open', 'print_', 'tryton-print', default=True),
|
|
])
|
|
print_ = StateReport('hotel.booking_status.report')
|
|
|
|
def do_print_(self, action):
|
|
company = self.start.company
|
|
data = {
|
|
'start_date': self.start.start_date,
|
|
'end_date': self.start.end_date,
|
|
'company': company.id,
|
|
'state': self.start.state,
|
|
'grouped': self.start.grouped,
|
|
}
|
|
return action, data
|
|
|
|
def transition_print_(self):
|
|
return 'end'
|
|
|
|
|
|
class BookingStatusReport(Report):
|
|
__name__ = 'hotel.booking_status.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')
|
|
dom = [
|
|
('arrival_date', '>=', data['start_date']),
|
|
('arrival_date', '<=', data['end_date']),
|
|
]
|
|
if data['state']:
|
|
dom.append(
|
|
('booking.state', '=', data['state'])
|
|
)
|
|
records = Folio.search(dom, order=[('arrival_date', 'ASC')])
|
|
|
|
_records = {}
|
|
for fol in records:
|
|
bk = fol.booking
|
|
if bk not in _records.keys():
|
|
_records[bk] = []
|
|
_records[bk].append(fol)
|
|
|
|
report_context['grouped'] = data['grouped']
|
|
report_context['records'] = _records
|
|
report_context['company'] = Company(data['company'])
|
|
return report_context
|
|
|
|
|
|
class InvoicePaymentForm(ModelView):
|
|
'Invoice Payment Form'
|
|
__name__ = 'invoice.payment.form'
|
|
payment_mode = fields.Many2One('account.voucher.paymode', 'Payment Mode',
|
|
required=True)
|
|
payment_amount = fields.Numeric('Payment amount', digits=(16, 2),
|
|
required=True)
|
|
party = fields.Many2One('party.party', 'Party', required=True)
|
|
pay_date = fields.Date('Advance Date', required=True)
|
|
reference = fields.Char('Reference')
|
|
|
|
@classmethod
|
|
def default_advance_date(cls):
|
|
Date = Pool().get('ir.date')
|
|
return Date.today()
|
|
|
|
|
|
class UpdateHolderStart(ModelView):
|
|
'Update Holder Start'
|
|
__name__ = 'hotel.update_holder.start'
|
|
name = fields.Char('Name', required=True)
|
|
sex = fields.Selection([
|
|
('', ''),
|
|
('male', 'Male'),
|
|
('female', 'Female'),
|
|
], 'Sex')
|
|
email = fields.Char('Email', required=True)
|
|
mobile = fields.Char('Mobile')
|
|
phone = fields.Char('Phone')
|
|
birthday = fields.Date('Birthday')
|
|
nationality = fields.Many2One('country.country', 'Nationality')
|
|
origin_country = fields.Many2One('party.country_code', 'Origin Country')
|
|
target_country = fields.Many2One('party.country_code', 'Target Country')
|
|
country = fields.Many2One('party.country_code', 'Country')
|
|
subdivision = fields.Many2One('party.department_code', 'Subdivision')
|
|
city = fields.Many2One('party.city_code', 'City', domain=[
|
|
('department', '=', Eval('subdivision'))
|
|
])
|
|
address = fields.Char('Address', required=True)
|
|
type_document = fields.Selection(TYPE_DOCUMENT, 'Tipo de Documento',
|
|
required=True)
|
|
id_number = fields.Char('Id Number', required=True)
|
|
visa_number = fields.Char('Visa Number')
|
|
visa_date = fields.Date('Visa Date')
|
|
notes = fields.Text('Notes')
|
|
|
|
customer = fields.Many2One('party.party', 'Party')
|
|
customer_id_number = fields.Char('Customer Id Number')
|
|
customer_name = fields.Char('Customer Name')
|
|
customer_country = fields.Many2One('party.country_code', 'Customer Country')
|
|
customer_subdivision = fields.Many2One('party.department_code',
|
|
'Customer Subdivision')
|
|
customer_city = fields.Many2One('party.city_code', 'Customer City')
|
|
customer_address = fields.Char('Customer Address')
|
|
customer_phone = fields.Char('Customer Phone')
|
|
customer_email = fields.Char('Customer Email')
|
|
customer_type_document = fields.Selection(TYPE_DOCUMENT,
|
|
'Customer Type Doc.')
|
|
main_guest = fields.Boolean('Main Guest')
|
|
vehicle_plate = fields.Char('Vehicle Plate')
|
|
|
|
@fields.depends('id_number', 'name', 'sex', 'email', 'mobile',
|
|
'visa_number', 'visa_date', 'address', 'birthday', 'nationality')
|
|
def on_change_id_number(self):
|
|
if self.id_number:
|
|
pool = Pool()
|
|
Party = pool.get('party.party')
|
|
Guest = pool.get('hotel.folio.guest')
|
|
clause = [('id_number', '=', self.id_number)]
|
|
parties = Party.search(clause)
|
|
if not parties:
|
|
parties = Guest.search(clause)
|
|
if not parties:
|
|
return
|
|
|
|
address = ''
|
|
country = None
|
|
subdivision = None
|
|
party = parties[0]
|
|
if hasattr(party, 'addresses') and party.addresses:
|
|
_address = party.addresses[0]
|
|
address = _address.street
|
|
country = _address.country_code and _address.country_code.id
|
|
subdivision = _address.department_code and _address.department_code.id
|
|
elif hasattr(party, 'address') and party.address:
|
|
address = party.address
|
|
|
|
self.name = party.name
|
|
self.sex = party.sex
|
|
self.email = party.email
|
|
self.mobile = party.mobile
|
|
self.visa_number = party.visa_number
|
|
self.visa_date = party.visa_date
|
|
self.birthday = party.birthday
|
|
self.nationality = party.nationality and party.nationality.id
|
|
self.country = country
|
|
self.subdivision = subdivision
|
|
self.address = address
|
|
|
|
|
|
class UpdateHolder(Wizard):
|
|
'Update Holder'
|
|
__name__ = 'hotel.update_holder'
|
|
start = StateView(
|
|
'hotel.update_holder.start',
|
|
'hotel.update_holder_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Update', 'update', 'tryton-ok', default=True),
|
|
])
|
|
update = StateTransition()
|
|
|
|
def default_start(self, fields):
|
|
pool = Pool()
|
|
Booking = pool.get('hotel.booking')
|
|
Config = pool.get('hotel.configuration')
|
|
config = Config.get_configuration()
|
|
booking = Booking(Transaction().context['active_id'])
|
|
party = booking.party
|
|
nationality_id = None
|
|
country_id = None
|
|
if config.nationality:
|
|
nationality_id = config.nationality.id
|
|
if config.country:
|
|
country_id = config.country.id
|
|
|
|
res = {
|
|
'nationality': nationality_id,
|
|
'country': country_id,
|
|
}
|
|
if party and party.id > 0:
|
|
if party.nationality:
|
|
nationality_id = party.nationality.id
|
|
|
|
address = party.addresses[0] if party.addresses else None
|
|
res = {
|
|
'name': party.name.upper(),
|
|
'nationality': nationality_id,
|
|
'id_number': party.id_number,
|
|
'type_document': party.type_document,
|
|
'sex': party.sex,
|
|
'mobile': party.mobile,
|
|
'phone': party.phone,
|
|
'email': party.email,
|
|
'visa_number': party.visa_number,
|
|
'visa_date': party.visa_date,
|
|
'birthday': party.birthday,
|
|
'notes': party.notes,
|
|
}
|
|
if address:
|
|
res['country'] = address.country_code.id if address.country_code else None
|
|
res['city'] = address.city_code.id if address.city_code else None
|
|
res['subdivision'] = address.department_code.id if address.department_code else None
|
|
elif booking.contact:
|
|
res['name'] = booking.contact.upper()
|
|
return res
|
|
|
|
def _set_cms(self, action, rec, email, mobile=None, phone=None):
|
|
cms = [] # contact_mechanisms
|
|
if mobile:
|
|
cms.append({'type': 'mobile', 'value': mobile})
|
|
if email:
|
|
cms.append({'type': 'email', 'value': email})
|
|
if phone:
|
|
cms.append({'type': 'phone', 'value': phone})
|
|
if cms:
|
|
rec['contact_mechanisms'] = [(action, cms)]
|
|
return
|
|
|
|
def transition_update(self):
|
|
pool = Pool()
|
|
Booking = pool.get('hotel.booking')
|
|
Folio = pool.get('hotel.folio')
|
|
Guest = pool.get('hotel.folio.guest')
|
|
Party = pool.get('party.party')
|
|
Address = pool.get('party.address')
|
|
CM = pool.get('party.contact_mechanism')
|
|
active_id = Transaction().context.get('active_id', False)
|
|
edit = True
|
|
booking = Booking(active_id)
|
|
_party = self.start
|
|
to_folio = {}
|
|
if _party.vehicle_plate:
|
|
to_folio['vehicle_plate'] = _party.vehicle_plate
|
|
|
|
nationality_id = _party.nationality.id if _party.nationality else None
|
|
rec = {
|
|
'name': _party.name.upper(),
|
|
'nationality': nationality_id,
|
|
'sex': _party.sex,
|
|
'birthday': _party.birthday,
|
|
'type_document': _party.type_document,
|
|
'id_number': _party.id_number,
|
|
'visa_number': _party.visa_number,
|
|
'visa_date': _party.visa_date,
|
|
'notes': _party.notes,
|
|
}
|
|
country_code = _party.country.id if _party.country else None
|
|
city_code = _party.city.id if _party.city else None
|
|
subdivision_code = _party.subdivision.id if _party.subdivision else None
|
|
street = _party.address.upper() if _party.address else ''
|
|
address = {}
|
|
address['country_code'] = country_code
|
|
address['city_code'] = city_code
|
|
address['department_code'] = subdivision_code
|
|
address['street'] = street
|
|
party = None
|
|
if not booking.party:
|
|
edit = False
|
|
parties = Party.search([
|
|
('id_number', '=', _party.id_number),
|
|
])
|
|
if not parties:
|
|
self._set_cms(
|
|
'create', rec, _party.email, _party.mobile, _party.phone
|
|
)
|
|
rec['addresses'] = [('create', [address])]
|
|
# raise UserError('Este cliente ya existe!')
|
|
else:
|
|
party = parties[0]
|
|
edit = True
|
|
else:
|
|
party = booking.party
|
|
|
|
if party:
|
|
if party.addresses:
|
|
Address.write(list(party.addresses), address)
|
|
else:
|
|
Address.create([address])
|
|
cms_add = {}
|
|
if _party.mobile:
|
|
cms_add['mobile'] = _party.mobile
|
|
cms_add['phone'] = _party.phone
|
|
cms_add['email'] = _party.email
|
|
if party.contact_mechanisms:
|
|
for cm in party.contact_mechanisms:
|
|
if cm.type == 'mobile' and _party.mobile:
|
|
cm.value = cms_add.pop('mobile')
|
|
elif cm.type == 'phone' and _party.phone:
|
|
cm.value = cms_add.pop('phone')
|
|
elif cm.type == 'email' and _party.email:
|
|
cm.value = cms_add.pop('email')
|
|
cm.save()
|
|
if cms_add:
|
|
for (key, value) in cms_add.items():
|
|
if not value:
|
|
continue
|
|
cm = CM(party=party.id, type=key, value=value)
|
|
cm.save()
|
|
else:
|
|
self._set_cms('create', rec, _party.email, _party.mobile, _party.phone)
|
|
|
|
rec_ = None
|
|
if _party.customer_id_number and _party.customer_name and _party.customer_type_document:
|
|
rec_ = {
|
|
'name': _party.customer_name,
|
|
'id_number': _party.customer_id_number,
|
|
'type_document': _party.customer_type_document,
|
|
}
|
|
address_cust = {
|
|
'country_code': None,
|
|
'city_code': None,
|
|
'street': '',
|
|
}
|
|
if _party.customer_country:
|
|
address_cust['country_code'] = _party.customer_country.id
|
|
if _party.customer_city:
|
|
address_cust['city_code'] = _party.customer_city.id
|
|
if _party.customer_address:
|
|
address_cust['street'] = _party.customer_address.upper()
|
|
|
|
rec_['addresses'] = [('create', [address_cust])]
|
|
self._set_cms(
|
|
'create',
|
|
rec_,
|
|
_party.customer_email,
|
|
phone=_party.customer_phone,
|
|
)
|
|
party, = Party.create([rec_])
|
|
if edit:
|
|
Party.write([party], rec)
|
|
if not booking.party:
|
|
booking.party = party.id
|
|
else:
|
|
party, = Party.create([rec])
|
|
Booking.write([booking], {'party': party.id})
|
|
|
|
booking.save()
|
|
if _party.type_document != '31':
|
|
for folio in booking.lines:
|
|
if not folio.main_guest:
|
|
folio.main_guest = party.id
|
|
folio.save()
|
|
_ = rec.pop('contact_mechanisms', None)
|
|
_ = rec.pop('addresses', None)
|
|
rec['address'] = address['street']
|
|
rec['country'] = country_code
|
|
rec['nationality'] = nationality_id
|
|
rec['folio'] = folio.id
|
|
rec['main_guest'] = True
|
|
rec['email'] = _party.email
|
|
rec['mobile'] = _party.mobile
|
|
Guest.create([rec])
|
|
break
|
|
|
|
for folio in booking.lines:
|
|
Folio.write([folio], to_folio)
|
|
return 'end'
|
|
|
|
|
|
class ManagerStart(ModelView):
|
|
'Manager Start'
|
|
__name__ = 'hotel.print_manager.start'
|
|
start_date = fields.Date('Start Date', required=True)
|
|
end_date = fields.Date('End Date', required=True)
|
|
company = fields.Many2One('company.company', 'Company', required=True)
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
|
|
class Manager(Wizard):
|
|
'Manager'
|
|
__name__ = 'hotel.print_manager'
|
|
start = StateView(
|
|
'hotel.print_manager.start',
|
|
'hotel.print_manager_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Open', 'print_', 'tryton-print', default=True),
|
|
])
|
|
print_ = StateReport('hotel.manager.report')
|
|
|
|
def do_print_(self, action):
|
|
company = self.start.company
|
|
data = {
|
|
'start_date': self.start.start_date,
|
|
'end_date': self.start.end_date,
|
|
'company': company.id,
|
|
}
|
|
return action, data
|
|
|
|
def transition_print_(self):
|
|
return 'end'
|
|
|
|
|
|
class ManagerReport(Report):
|
|
__name__ = 'hotel.manager.report'
|
|
|
|
@classmethod
|
|
def get_location(self, party, field):
|
|
for address in party.addresses:
|
|
if hasattr(address, field):
|
|
value = getattr(address, field + '_code')
|
|
if value:
|
|
return value
|
|
|
|
@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')
|
|
Room = pool.get('hotel.room')
|
|
User = pool.get('res.user')
|
|
rooms = Room.search([])
|
|
total_rooms = len(rooms)
|
|
delta_days = (data['end_date'] - data['start_date']).days + 1
|
|
folios = Folio.search([
|
|
('arrival_date', '>=', data['start_date']),
|
|
('arrival_date', '<=', data['end_date']),
|
|
])
|
|
list_channels = [
|
|
('ota', 'OTAS'),
|
|
('direct', 'DIRECTOS'),
|
|
('special_price', 'TARIFA ESPECIAL'),
|
|
('courtesy', 'CORTESIA'),
|
|
('house_use', 'USO DE CASA'),
|
|
]
|
|
|
|
channels = {}
|
|
rooms_occupied = []
|
|
for ch, desc in list_channels:
|
|
channels[ch] = {
|
|
'name': desc,
|
|
'room_qty': [],
|
|
'room_rate': 0,
|
|
'num_adults': [],
|
|
'num_children': [],
|
|
'income': [],
|
|
'adr': 0,
|
|
'rev_par': 0,
|
|
}
|
|
|
|
local_guests = []
|
|
foreign_guests = []
|
|
total_income = []
|
|
rooms_saled = []
|
|
guests_by_country = {}
|
|
guests_by_city = {}
|
|
for folio in folios:
|
|
if folio.booking.channel:
|
|
type_ = 'ota'
|
|
else:
|
|
type_ = 'direct'
|
|
channels[type_]['room_qty'].append(folio.nights_quantity)
|
|
channels[type_]['num_adults'].append(folio.num_adults)
|
|
channels[type_]['num_children'].append(folio.num_children)
|
|
channels[type_]['income'].append(folio.total_amount)
|
|
total_income.append(folio.total_amount)
|
|
for guest in folio.guests:
|
|
if not guest or not guest.nationality:
|
|
continue
|
|
if guest.nationality.name == 'COLOMBIA':
|
|
city = cls.get_location(folio.main_guest, 'city')
|
|
local_guests.append(1)
|
|
if not city:
|
|
continue
|
|
if city not in guests_by_city.keys():
|
|
guests_by_city[city] = {
|
|
'name': city.name,
|
|
'persons': [],
|
|
'nights': [],
|
|
}
|
|
guests_by_city[city]['persons'].append(1)
|
|
guests_by_city[city]['nights'].append(folio.nights_quantity)
|
|
else:
|
|
country = cls.get_location(folio.main_guest, 'country')
|
|
foreign_guests.append(1)
|
|
if not country:
|
|
continue
|
|
if country not in guests_by_country.keys():
|
|
guests_by_country[country] = {
|
|
'name': country.name,
|
|
'persons': [],
|
|
'nights': [],
|
|
}
|
|
|
|
guests_by_country[country]['persons'].append(1)
|
|
guests_by_country[country]['nights'].append(folio.nights_quantity)
|
|
|
|
for k, v in channels.items():
|
|
room_qty = sum(v['room_qty'])
|
|
income = sum(v['income'])
|
|
v['room_qty'] = room_qty
|
|
v['room_rate'] = room_qty / (total_rooms * delta_days)
|
|
v['num_adults'] = sum(v['num_adults'])
|
|
v['num_children'] = sum(v['num_children'])
|
|
v['income'] = income
|
|
if income > 0:
|
|
v['adr'] = income / room_qty
|
|
v['rev_par'] = income / delta_days
|
|
rooms_occupied.append(room_qty)
|
|
if k in ('direct', 'ota'):
|
|
rooms_saled.append(room_qty)
|
|
|
|
available_nights = total_rooms * delta_days
|
|
beds_capacity = []
|
|
for room in rooms:
|
|
beds_capacity.append(room.main_accommodation.accommodation_capacity or 0)
|
|
|
|
available_beds = sum(beds_capacity) * delta_days
|
|
average_price = sum(total_income) / sum(rooms_saled)
|
|
report_context['records'] = channels.values()
|
|
report_context['rooms_occupied'] = sum(rooms_occupied)
|
|
report_context['rooms_saled'] = sum(rooms_saled)
|
|
report_context['gross_occupancy'] = sum(rooms_occupied) / available_nights
|
|
report_context['net_occupancy'] = sum(rooms_saled) / available_nights
|
|
report_context['total_income'] = sum(total_income)
|
|
report_context['local_guests'] = sum(local_guests)
|
|
report_context['foreign_guests'] = sum(foreign_guests)
|
|
report_context['available_nights'] = available_nights
|
|
report_context['available_beds'] = available_beds
|
|
report_context['average_price'] = average_price
|
|
report_context['guests_by_country'] = guests_by_country.values()
|
|
report_context['guests_by_city'] = guests_by_city.values()
|
|
report_context['company'] = Company(data['company'])
|
|
user_id = Transaction().context.get('user')
|
|
report_context['user'] = User(user_id)
|
|
return report_context
|
|
|
|
|
|
class BookingChannelCommision(ModelSQL):
|
|
'Booking Channel Commision'
|
|
__name__ = 'hotel.booking-channel.commission'
|
|
_table = 'hotel_booking_channel_commission_rel'
|
|
commission = fields.Many2One('hotel.channel.commission',
|
|
'Channel Commission', ondelete='CASCADE', required=True)
|
|
booking = fields.Many2One('hotel.booking', 'Booking', ondelete='RESTRICT',
|
|
required=True)
|
|
|
|
|
|
class StatementPaymentForm(ModelView):
|
|
'Statement Payment Form'
|
|
__name__ = 'hotel.payment_form.start'
|
|
statement = fields.Many2One('account.statement', 'Statement',
|
|
required=True, domain=['OR', [
|
|
('create_uid.login', '=', Eval('user')),
|
|
('state', '=', 'draft')
|
|
], [
|
|
Eval('user') == 'admin',
|
|
('state', '=', 'draft'),
|
|
]])
|
|
amount_to_pay = fields.Numeric('Amount to Pay', required=True,
|
|
digits=(16, Eval('currency_digits', 2)),
|
|
depends=['currency_digits'])
|
|
currency_digits = fields.Integer('Currency Digits')
|
|
kind = fields.Selection([
|
|
('booking', 'Booking'),
|
|
('folio', 'Folio'),
|
|
], 'Kind', required=True)
|
|
folio = fields.Many2One('hotel.folio', 'Folio', domain=[
|
|
('id', 'in', Eval('folios')),
|
|
], states={
|
|
'invisible': Eval('kind') == 'booking',
|
|
'required': Eval('kind') == 'folio',
|
|
}, depends=['folios', 'kind']
|
|
)
|
|
voucher = fields.Char('Voucher', states={
|
|
'required': Bool(Eval('require_voucher')),
|
|
}, depends=['require_voucher'])
|
|
party = fields.Many2One('party.party', 'Party', domain=[
|
|
('id', 'in', Eval('parties'))
|
|
], required=True)
|
|
user = fields.Many2One('res.user', 'User', states={'readonly': True})
|
|
require_voucher = fields.Boolean('Require Voucher', depends=['statement'])
|
|
parties = fields.Many2Many('party.party', None, None, 'Parties')
|
|
folios = fields.Many2Many('hotel.folio', None, None, 'Folios')
|
|
|
|
@classmethod
|
|
def default_require_voucher(cls):
|
|
return False
|
|
|
|
@classmethod
|
|
def default_user(cls):
|
|
user = Pool().get('res.user')(Transaction().user)
|
|
return user.id
|
|
|
|
@classmethod
|
|
def default_kind(cls):
|
|
model = Transaction().context['active_model']
|
|
if model == "hotel.folio":
|
|
return "folio"
|
|
else:
|
|
return "booking"
|
|
|
|
@fields.depends('statement', 'require_voucher')
|
|
def on_change_with_require_voucher(self):
|
|
if self.statement:
|
|
return self.statement.journal.require_voucher
|
|
return False
|
|
|
|
|
|
class WizardStatementPayment(Wizard):
|
|
'Wizard Statement Payment'
|
|
__name__ = 'hotel.payment_form'
|
|
start = StateView(
|
|
'hotel.payment_form.start',
|
|
'hotel.statement_payment_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Pay', 'pay_', 'tryton-ok', default=True),
|
|
])
|
|
pay_ = StateTransition()
|
|
|
|
def default_start(self, fields):
|
|
pool = Pool()
|
|
Booking = pool.get('hotel.booking')
|
|
Folio = pool.get('hotel.folio')
|
|
model = Transaction().context['active_model']
|
|
active_id = Transaction().context['active_id']
|
|
parties_ids = []
|
|
folios_ids = []
|
|
folio_id = None
|
|
party_id = None
|
|
pending_to_pay = 0
|
|
kind = 'booking'
|
|
if model == "hotel.booking":
|
|
booking = Booking(active_id)
|
|
if not booking.party:
|
|
raise UserError(gettext('hotel.msg_missing_party_holder'))
|
|
|
|
if booking.responsible_payment in ('holder', 'holder_guest'):
|
|
parties_ids.append(booking.party.id)
|
|
if booking.responsible_payment in ('guest', 'holder_guest'):
|
|
for folio in booking.lines:
|
|
parties_ids.append(folio.main_guest.id)
|
|
folios_ids.append(folio.id)
|
|
pending_to_pay = booking.pending_to_pay
|
|
else:
|
|
kind = 'folio'
|
|
folio = Folio(active_id)
|
|
folios_ids.append(folio.id)
|
|
parties_ids.append(folio.main_guest.id)
|
|
if folio.booking.responsible_payment == 'guest':
|
|
pending_to_pay = folio.pending_total
|
|
elif folio.booking.responsible_payment == 'holder_guest':
|
|
pending_to_pay = folio.pending_charges
|
|
if folio.booking.party == folio.main_guest and \
|
|
folio.booking.responsible_payment == 'holder':
|
|
pending_to_pay = folio.booking.pending_to_pay
|
|
|
|
return {
|
|
'currency_digits': 2,
|
|
'party': party_id,
|
|
'folio': folio_id,
|
|
'parties': parties_ids,
|
|
'folios': folios_ids,
|
|
'amount_to_pay': pending_to_pay,
|
|
'kind': kind,
|
|
}
|
|
|
|
def transition_pay_(self):
|
|
pool = Pool()
|
|
Booking = pool.get('hotel.booking')
|
|
Folio = pool.get('hotel.folio')
|
|
StatementLine = pool.get('account.statement.line')
|
|
active_id = Transaction().context.get('active_id', False)
|
|
|
|
form = self.start
|
|
target_model = 'booking'
|
|
if form.kind == 'booking':
|
|
record = Booking(active_id)
|
|
description = record.number
|
|
else:
|
|
target_model = 'folio'
|
|
record = Folio(form.folio.id)
|
|
if record.booking.responsible_payment != 'holder':
|
|
description = record.registration_card
|
|
else:
|
|
booking = record.booking
|
|
description = booking.number
|
|
target_model = 'booking'
|
|
|
|
try:
|
|
account = form.party.account_receivable.id
|
|
except:
|
|
raise UserError(gettext(
|
|
'hotel.party_without_account_receivable'))
|
|
|
|
number = ''
|
|
if form.voucher:
|
|
number = form.voucher
|
|
|
|
if form.amount_to_pay:
|
|
line = StatementLine(
|
|
statement=form.statement.id,
|
|
date=date.today(),
|
|
amount=form.amount_to_pay,
|
|
party=form.party.id,
|
|
account=account,
|
|
number=number,
|
|
description=description,
|
|
voucher=self.start.voucher,
|
|
source=str(record),
|
|
)
|
|
line.save()
|
|
line.create_move()
|
|
|
|
# We keep to mark all concepts charges/folios as paid if
|
|
# the customer to pay all pending values
|
|
if form.kind == 'folio':
|
|
if target_model == 'folio':
|
|
folio = record
|
|
if folio.booking.responsible_payment == 'guest':
|
|
pending = folio.pending_total
|
|
elif folio.booking.responsible_payment == 'holder_guest':
|
|
pending = folio.pending_charges
|
|
if form.amount_to_pay >= pending:
|
|
for charge in record.charges:
|
|
if charge.status == 'paid':
|
|
continue
|
|
charge.status = 'paid'
|
|
charge.save()
|
|
folio.payment_status = 'paid'
|
|
|
|
if target_model == 'booking':
|
|
if form.amount_to_pay >= record.pending_to_pay:
|
|
for folio in record.lines:
|
|
for charge in folio.charges:
|
|
if charge.status == 'paid':
|
|
continue
|
|
charge.status = 'paid'
|
|
charge.save()
|
|
folio.payment_status = 'paid'
|
|
folio.save()
|
|
|
|
record.save()
|
|
return 'end'
|
|
|
|
|
|
class BillBookingStart(ModelView):
|
|
'Bill Booking Form'
|
|
__name__ = 'hotel.bill_booking.start'
|
|
kind = fields.Selection([
|
|
('all', 'All'),
|
|
('only_accommodation', 'Only Accommodation'),
|
|
('only_products', 'Only Products'),
|
|
], 'Kind', required=True)
|
|
party = fields.Many2One('party.party', 'Bill To', domain=[
|
|
('id', 'in', Eval('parties')),
|
|
])
|
|
parties = fields.Many2Many('party.party', None, None, 'Parties')
|
|
|
|
|
|
class BillBooking(Wizard):
|
|
'Bill Booking'
|
|
__name__ = 'hotel.bill_booking'
|
|
start = StateView(
|
|
'hotel.bill_booking.start',
|
|
'hotel.bill_booking_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Invoice', 'bill', 'tryton-ok', default=True),
|
|
])
|
|
bill = StateTransition()
|
|
|
|
def default_start(self, fields):
|
|
pool = Pool()
|
|
Booking = pool.get('hotel.booking')
|
|
Folio = pool.get('hotel.folio')
|
|
active_id = Transaction().context.get('active_id', False)
|
|
active_model = Transaction().context.get('active_model')
|
|
parties_ids = []
|
|
if active_model == 'hotel.booking':
|
|
bk = Booking(active_id)
|
|
if bk.party:
|
|
parties_ids = [bk.party.id]
|
|
for fo in bk.lines:
|
|
if fo.main_guest:
|
|
parties_ids.append(fo.main_guest.id)
|
|
else:
|
|
folio = Folio(active_id)
|
|
parties_ids = [folio.id]
|
|
return {
|
|
'parties': parties_ids,
|
|
}
|
|
|
|
def transition_bill(self):
|
|
pool = Pool()
|
|
Booking = pool.get('hotel.booking')
|
|
Folio = pool.get('hotel.folio')
|
|
active_id = Transaction().context.get('active_id', False)
|
|
active_model = Transaction().context.get('active_model')
|
|
folios = []
|
|
if active_model == 'hotel.booking':
|
|
bk = Booking(active_id)
|
|
folios = bk.lines
|
|
else:
|
|
folio = Folio(active_id)
|
|
folios = [folio]
|
|
bk = folio.booking
|
|
|
|
Booking.create_invoice(folios, kind=self.start.kind,
|
|
party=self.start.party)
|
|
Booking.concile_charges([bk])
|
|
Booking.check_finished([bk])
|
|
return 'end'
|
|
|
|
|
|
class OperationForecastStart(ModelView):
|
|
'Operation Forecast Start'
|
|
__name__ = 'hotel.print_operation_forecast.start'
|
|
company = fields.Many2One('company.company', 'Company', required=True)
|
|
fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscalyear',
|
|
required=True)
|
|
period = fields.Many2One('account.period', 'Period', required=True,
|
|
domain=[
|
|
('fiscalyear', '=', Eval('fiscalyear')),
|
|
])
|
|
|
|
@staticmethod
|
|
def default_fiscalyear():
|
|
Fiscalyear = Pool().get('account.fiscalyear')
|
|
fiscalyear, = Fiscalyear.search([], limit=1, order=[
|
|
('start_date', 'DESC')])
|
|
return fiscalyear.id
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
|
|
class OperationForecast(Wizard):
|
|
'Operation Forecast'
|
|
__name__ = 'hotel.print_operation_forecast'
|
|
start = StateView(
|
|
'hotel.print_operation_forecast.start',
|
|
'hotel.print_operation_forecast_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Print', 'print_', 'tryton-print', default=True),
|
|
])
|
|
print_ = StateReport('hotel.operation_forecast.report')
|
|
|
|
def do_print_(self, action):
|
|
company = self.start.company
|
|
data = {
|
|
'period': self.start.period.id,
|
|
'company': company.id,
|
|
}
|
|
return action, data
|
|
|
|
def transition_print_(self):
|
|
return 'end'
|
|
|
|
|
|
class OperationForecastReport(Report):
|
|
"Operation Forecast Report"
|
|
__name__ = 'hotel.operation_forecast.report'
|
|
|
|
@classmethod
|
|
def get_context(cls, records, header, data):
|
|
report_context = super().get_context(records, header, data)
|
|
pool = Pool()
|
|
Company = pool.get('company.company')
|
|
Period = pool.get('account.period')
|
|
Room = pool.get('hotel.room')
|
|
Maintenance = pool.get('hotel.maintenance')
|
|
Occupancy = pool.get('hotel.folio.occupancy')
|
|
|
|
period = Period(data['period'])
|
|
start_date = period.start_date
|
|
end_date = period.end_date
|
|
|
|
range_day = (end_date - start_date).days + 1
|
|
default_data = {
|
|
'available_rooms': 0,
|
|
'occupied_rooms': 0,
|
|
'pax': 0,
|
|
'rooms_complementary': 0,
|
|
'info_complementary': '',
|
|
'rooms_maintenance': 0,
|
|
'occupancy_rate': 0,
|
|
'revenue': 0,
|
|
'balance': 0,
|
|
'average_price': 0,
|
|
}
|
|
|
|
records = []
|
|
|
|
rooms = Room.search([])
|
|
pax = []
|
|
occupied_rooms = []
|
|
revenue = []
|
|
balance = 0
|
|
|
|
totals = copy.deepcopy(default_data)
|
|
today = date.today()
|
|
for key, value in default_data.items():
|
|
totals[key + '_effective'] = []
|
|
totals[key + '_forecast'] = []
|
|
|
|
available_rooms = 0
|
|
for nday in range(range_day):
|
|
available_rooms = len(rooms)
|
|
rooms_complementary = 0
|
|
rooms_maintenance = 0
|
|
tdate = start_date + timedelta(nday)
|
|
data_day = copy.deepcopy(default_data)
|
|
data_day['date'] = tdate
|
|
is_sunday = 'no'
|
|
if tdate.weekday() == 6:
|
|
is_sunday = 'yes'
|
|
data_day['sunday'] = is_sunday
|
|
occupancies = Occupancy.search([
|
|
('occupancy_date', '=', str(tdate))
|
|
])
|
|
occ_rooms = len(occupancies)
|
|
|
|
mnts_rooms = Maintenance.search_read([
|
|
('start_date', '<=', tdate),
|
|
('end_date', '>=', tdate),
|
|
])
|
|
rooms_maintenance = len(mnts_rooms)
|
|
data_day['rooms_maintenance'] = rooms_maintenance
|
|
if mnts_rooms:
|
|
available_rooms -= rooms_maintenance
|
|
|
|
data_day['available_rooms'] = available_rooms
|
|
data_day['occupied_rooms'] = occ_rooms
|
|
occupied_rooms.append(occ_rooms)
|
|
|
|
sum_pax = sum([len(occ.folio.guests) for occ in occupancies])
|
|
data_day['pax'] = sum_pax
|
|
pax.append(sum_pax)
|
|
amounts = []
|
|
for occ in occupancies:
|
|
if occ.charge:
|
|
amount = occ.charge.unit_price
|
|
else:
|
|
amount = occ.unit_price
|
|
if occ.folio.booking.complementary:
|
|
rooms_complementary += 1
|
|
amounts.append(amount)
|
|
|
|
data_day['rooms_complementary'] = rooms_complementary
|
|
total_amounts = sum(amounts)
|
|
data_day['revenue'] = total_amounts
|
|
revenue.append(total_amounts)
|
|
|
|
balance += total_amounts
|
|
data_day['balance'] = balance
|
|
data_day['occupancy_rate'] = occ_rooms / available_rooms
|
|
|
|
average_price = None
|
|
if occ_rooms:
|
|
average_price = round(total_amounts / occ_rooms, 2)
|
|
data_day['average_price'] = average_price
|
|
records.append(data_day)
|
|
|
|
if tdate < today:
|
|
row = '_effective'
|
|
else:
|
|
row = '_forecast'
|
|
|
|
totals['available_rooms' + row].append(available_rooms)
|
|
totals['occupied_rooms' + row].append(occ_rooms)
|
|
totals['pax' + row].append(sum_pax)
|
|
totals['rooms_complementary' + row].append(rooms_complementary)
|
|
totals['rooms_maintenance' + row].append(rooms_maintenance)
|
|
totals['revenue' + row].append(total_amounts)
|
|
|
|
for field in (
|
|
'available_rooms', 'occupied_rooms', 'pax',
|
|
'rooms_complementary', 'revenue', 'rooms_maintenance'):
|
|
_effective = field + '_effective'
|
|
_forecast = field + '_forecast'
|
|
totals[_effective] = sum(totals[_effective])
|
|
totals[_forecast] = sum(totals[_forecast])
|
|
|
|
average_price_effective = ''
|
|
average_price_forecast = ''
|
|
occupancy_rate_effective = ''
|
|
occupancy_rate_forecast = ''
|
|
if totals['occupied_rooms_effective'] > 0:
|
|
average_price_effective = totals['revenue_effective'] / totals['occupied_rooms_effective']
|
|
if totals['occupied_rooms_forecast'] > 0:
|
|
average_price_forecast = totals['revenue_forecast'] / totals['occupied_rooms_forecast']
|
|
if totals['available_rooms_effective'] > 0:
|
|
occupancy_rate_effective = totals['occupied_rooms_effective'] / totals['available_rooms_effective']
|
|
if totals['available_rooms_forecast'] > 0:
|
|
occupancy_rate_forecast = totals['occupied_rooms_forecast'] / totals['available_rooms_forecast']
|
|
|
|
totals['occupancy_rate_effective'] = occupancy_rate_effective
|
|
totals['occupancy_rate_forecast'] = occupancy_rate_forecast
|
|
totals['average_price_effective'] = average_price_effective
|
|
totals['average_price_forecast'] = average_price_forecast
|
|
data.update(totals)
|
|
total_available_rooms = len(rooms) * range_day
|
|
total_occupied_rooms = sum(occupied_rooms)
|
|
data['available_rooms'] = total_available_rooms
|
|
data['occupied_rooms'] = total_occupied_rooms
|
|
data['pax'] = sum(pax)
|
|
data['revenue'] = sum(revenue)
|
|
data['balance'] = balance
|
|
_average_price = None
|
|
_occupancy_rate = None
|
|
if total_occupied_rooms > 0:
|
|
_average_price = sum(revenue) / sum(occupied_rooms)
|
|
if total_available_rooms > 0:
|
|
_occupancy_rate = round(
|
|
total_occupied_rooms / total_available_rooms, 2)
|
|
data['average_price'] = _average_price
|
|
data['occupancy_rate'] = _occupancy_rate
|
|
|
|
report_context['records'] = records
|
|
report_context['period'] = period.name
|
|
report_context['company'] = Company(data['company'])
|
|
return report_context
|
|
|
|
|
|
class RevenueSegmentationStart(ModelView):
|
|
'Revenue Segmentation Start'
|
|
__name__ = 'hotel.revenue_segmentation.start'
|
|
company = fields.Many2One('company.company', 'Company', required=True)
|
|
fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscalyear',
|
|
required=True)
|
|
kind = fields.Selection([
|
|
('by_day', 'By Day'),
|
|
('by_months', 'By Months'),
|
|
], 'Kind', required=True)
|
|
period = fields.Many2One('account.period', 'Period', states={
|
|
'invisible': Eval('kind') != 'by_day',
|
|
'required': Eval('kind') == 'by_day'
|
|
},
|
|
domain=[
|
|
('fiscalyear', '=', Eval('fiscalyear')),
|
|
])
|
|
periods = fields.Many2Many('account.period', None, None, 'Periods',
|
|
states={
|
|
'required': Eval('kind') == 'by_months',
|
|
'invisible': Eval('kind') != 'by_months',
|
|
}, domain=[
|
|
('fiscalyear', '=', Eval('fiscalyear')),
|
|
])
|
|
|
|
@staticmethod
|
|
def default_fiscalyear():
|
|
Fiscalyear = Pool().get('account.fiscalyear')
|
|
fiscalyear, = Fiscalyear.search([], limit=1, order=[
|
|
('start_date', 'DESC')])
|
|
return fiscalyear.id
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
|
|
class RevenueSegmentation(Wizard):
|
|
'Revenue Segmentation'
|
|
__name__ = 'hotel.revenue_segmentation'
|
|
start = StateView(
|
|
'hotel.revenue_segmentation.start',
|
|
'hotel.revenue_segmentation_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Print', 'print_', 'tryton-print', default=True),
|
|
])
|
|
print_ = StateReport('hotel.revenue_segmentation.report')
|
|
|
|
def do_print_(self, action):
|
|
company = self.start.company
|
|
data = {
|
|
'company': company.id,
|
|
'kind': self.start.kind,
|
|
'period': self.start.period.id if self.start.period else None,
|
|
'periods': [period.id for period in self.start.periods],
|
|
}
|
|
return action, data
|
|
|
|
def transition_print_(self):
|
|
return 'end'
|
|
|
|
|
|
class RevenueSegmentationReport(Report):
|
|
"Revenue Segmentation Report"
|
|
__name__ = 'hotel.revenue_segmentation.report'
|
|
|
|
@classmethod
|
|
def _set_indicators(cls, dict_data, record, price):
|
|
record_id = record.id
|
|
if record_id not in dict_data.keys():
|
|
dict_data[record_id] = {
|
|
'name': record.name,
|
|
'nights': [],
|
|
'amount': [],
|
|
'average_price': 0,
|
|
}
|
|
dict_data[record_id]['amount'].append(price)
|
|
dict_data[record_id]['nights'].append(1)
|
|
|
|
@classmethod
|
|
def _compute_segments(cls, data, segments):
|
|
for segment in segments:
|
|
_type, values = segment
|
|
total_amount = []
|
|
total_nights = []
|
|
for _id, value in values.items():
|
|
amount = sum(value['amount'])
|
|
nights = sum(value['nights'])
|
|
total_amount.append(amount)
|
|
total_nights.append(nights)
|
|
value['amount'] = amount
|
|
value['nights'] = nights
|
|
value['average_price'] = amount / nights
|
|
|
|
data[_type + '_nights'] = sum(total_nights)
|
|
data[_type + '_amount'] = sum(total_amount)
|
|
average_price = ''
|
|
if total_nights:
|
|
average_price = sum(total_amount) / sum(total_nights)
|
|
data[_type + '_average_price'] = average_price
|
|
|
|
@classmethod
|
|
def _get_days_data(cls, period, _data, default_data):
|
|
pool = Pool()
|
|
Occupancy = pool.get('hotel.folio.occupancy')
|
|
channels = {}
|
|
corporative = {}
|
|
groups = {}
|
|
records = []
|
|
start_date = period.start_date
|
|
end_date = period.end_date
|
|
range_day = (end_date - start_date).days + 1
|
|
for nday in range(range_day):
|
|
tdate = start_date + timedelta(nday)
|
|
data_day = copy.deepcopy(default_data)
|
|
data_day['date'] = tdate
|
|
occupancies = Occupancy.search([
|
|
('occupancy_date', '=', str(tdate))
|
|
])
|
|
|
|
for occ in occupancies:
|
|
price = occ.unit_price
|
|
booking = occ.folio.booking
|
|
field = 'direct'
|
|
record = None
|
|
if booking.channel:
|
|
channel = booking.channel
|
|
field = channel.code
|
|
seg_data = channels
|
|
if field not in ('booking', 'expedia'):
|
|
field = 'other'
|
|
record = channel
|
|
elif booking.corporative:
|
|
field = 'corporative'
|
|
if booking.party:
|
|
record = booking.party
|
|
seg_data = corporative
|
|
elif booking.group:
|
|
field = 'group'
|
|
if booking.party:
|
|
record = booking.party
|
|
seg_data = groups
|
|
elif booking.media == 'web':
|
|
field = 'web'
|
|
if record:
|
|
cls._set_indicators(seg_data, record, price)
|
|
|
|
data_day[field].append(price)
|
|
data_day['room_' + field].append(1)
|
|
|
|
_data[field].append(price)
|
|
_data['room_' + field].append(1)
|
|
|
|
records.append(data_day)
|
|
|
|
segments = [
|
|
('channels', channels),
|
|
('corporative', corporative),
|
|
('groups', groups),
|
|
]
|
|
cls._compute_segments(_data, segments)
|
|
return records, channels, corporative, groups
|
|
|
|
@classmethod
|
|
def _get_months_data(cls, periods, _data, default_data):
|
|
Occupancy = Pool().get('hotel.folio.occupancy')
|
|
channels = {}
|
|
corporative = {}
|
|
groups = {}
|
|
records = []
|
|
for period in periods:
|
|
_month = {
|
|
'date': period.name,
|
|
}
|
|
_month.update(copy.deepcopy(default_data))
|
|
start = period.start_date
|
|
end = period.end_date
|
|
days = (end - start).days + 1
|
|
|
|
_dates = [start + timedelta(nday) for nday in range(days)]
|
|
occupancies = Occupancy.search([
|
|
('occupancy_date', 'in', _dates)
|
|
])
|
|
|
|
for occ in occupancies:
|
|
price = occ.unit_price
|
|
booking = occ.folio.booking
|
|
field = 'direct'
|
|
record = None
|
|
if booking.channel:
|
|
field = booking.channel.code
|
|
if field not in ('booking', 'expedia'):
|
|
field = 'other'
|
|
record = booking.channel
|
|
seg_data = channels
|
|
elif booking.corporative:
|
|
field = 'corporative'
|
|
record = booking.party
|
|
seg_data = corporative
|
|
elif booking.group:
|
|
field = 'group'
|
|
record = booking.party
|
|
seg_data = groups
|
|
elif booking.media == 'web':
|
|
field = 'web'
|
|
|
|
if record:
|
|
cls._set_indicators(seg_data, record, price)
|
|
_month[field].append(price)
|
|
_month['room_' + field].append(1)
|
|
|
|
_data[field].append(price)
|
|
_data['room_' + field].append(1)
|
|
|
|
records.append(_month)
|
|
|
|
segments = [
|
|
('channels', channels),
|
|
('corporative', corporative),
|
|
('groups', groups),
|
|
]
|
|
cls._compute_segments(_data, segments)
|
|
return records, channels, corporative, groups
|
|
|
|
@classmethod
|
|
def get_context(cls, records, header, data):
|
|
report_context = super().get_context(records, header, data)
|
|
pool = Pool()
|
|
Company = pool.get('company.company')
|
|
Period = pool.get('account.period')
|
|
data.update({
|
|
'channels_nights': 0,
|
|
'channels_amount': 0,
|
|
'channels_average_price': 0,
|
|
'corporative_nights': 0,
|
|
'corportative_amount': 0,
|
|
'corportative_average_price': 0,
|
|
})
|
|
|
|
default_data = {
|
|
'complementary': [],
|
|
'room_complementary': [],
|
|
'booking': [],
|
|
'room_booking': [],
|
|
'expedia': [],
|
|
'room_expedia': [],
|
|
'other': [],
|
|
'room_other': [],
|
|
'web': [],
|
|
'room_web': [],
|
|
'direct': [],
|
|
'room_direct': [],
|
|
'corporative': [],
|
|
'room_corporative': [],
|
|
'group': [],
|
|
'room_group': [],
|
|
}
|
|
_data = copy.deepcopy(default_data)
|
|
|
|
records = []
|
|
if data['kind'] == 'by_day':
|
|
period = Period(data['period'])
|
|
records, channels, corporative, groups = cls._get_days_data(
|
|
period, _data, default_data
|
|
)
|
|
desc_periods = period.name
|
|
else:
|
|
periods = Period.browse(data['periods'])
|
|
records, channels, corporative, groups = cls._get_months_data(
|
|
periods, _data, default_data)
|
|
desc_periods = ' | '.join([pd.name for pd in periods])
|
|
|
|
for rec in records:
|
|
for key, values in rec.items():
|
|
if key == 'date':
|
|
continue
|
|
rec[key] = sum(values)
|
|
|
|
for key, value in default_data.items():
|
|
if key == 'date':
|
|
continue
|
|
_data[key] = sum(_data[key])
|
|
|
|
data.update(_data)
|
|
report_context['records'] = records
|
|
report_context['channels'] = channels.values()
|
|
report_context['corporative'] = corporative.values()
|
|
report_context['groups'] = groups.values()
|
|
report_context['periods'] = desc_periods
|
|
report_context['company'] = Company(data['company'])
|
|
return report_context
|
|
|
|
|
|
class UpdateTaxesStart(ModelView):
|
|
'Update Taxes Start'
|
|
__name__ = 'hotel.booking.update_taxes.start'
|
|
DOMAIN = [
|
|
('parent', '=', None),
|
|
['OR',
|
|
('group', '=', None),
|
|
('group.kind', 'in', ['sale', 'both'])
|
|
],
|
|
]
|
|
taxes_to_add = fields.Many2Many('account.tax', None, None,
|
|
'Taxes to Add', domain=DOMAIN)
|
|
taxes_to_remove = fields.Many2Many('account.tax', None, None,
|
|
'Taxes to Remove', domain=DOMAIN)
|
|
|
|
|
|
class UpdateTaxes(Wizard):
|
|
'Update Taxes'
|
|
__name__ = 'hotel.booking.update_taxes'
|
|
start = StateView(
|
|
'hotel.booking.update_taxes.start',
|
|
'hotel.booking_update_taxes_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Ok', 'accept', 'tryton-ok', default=True),
|
|
])
|
|
accept = StateTransition()
|
|
|
|
def transition_accept(self):
|
|
pool = Pool()
|
|
Booking = pool.get('hotel.booking')
|
|
Charge = Pool().get('hotel.folio.charge')
|
|
active_id = Transaction().context.get('active_id', False)
|
|
booking = Booking(active_id)
|
|
to_update = []
|
|
if self.start.taxes_to_add:
|
|
to_update.append(('add', self.start.taxes_to_add))
|
|
if self.start.taxes_to_remove:
|
|
to_update.append(('remove', self.start.taxes_to_remove))
|
|
if to_update:
|
|
for folio in booking.lines:
|
|
Charge.write(list(folio.charges), {'taxes': to_update})
|
|
return 'end'
|