trytonpsk-hotel/booking.py
oscar alvarez 3e494ab79b Fix
2023-04-24 11:30:46 -05:00

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'