trytonpsk-hotel/folio.py

1560 lines
54 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 decimal import Decimal
from datetime import timedelta, datetime, date
from trytond.model import ModelView, Workflow, ModelSQL, fields
from trytond.pyson import Eval, Bool
from trytond.pool import Pool
from trytond.report import Report
from trytond.wizard import (
Wizard, StateView, StateAction, Button, StateTransition, StateReport
)
from trytond.transaction import Transaction
from trytond.model.exceptions import AccessError
from trytond.exceptions import UserError
from trytond.i18n import gettext
from .constants import REGISTRATION_STATE, COMPLEMENTARY, INVOICE_STATES
STATES_CHECKIN = {
'readonly': Eval('registration_state').in_(
['check_in', 'no_show', 'cancelled']
),
'required': Eval('registration_state') == 'check_in',
}
STATES_CHECKOUT = {
'readonly': Eval('registration_state').in_(
['check_out', 'no_show', 'cancelled']
),
'required': Eval('registration_state') == 'check_in',
}
STATES_OP = {
'readonly': Eval('state').in_(['check_out', 'done', 'cancelled'])
}
STATES_MNT = {
'readonly': Eval('state') != 'draft',
}
COLOR_BOOKING = {
'draft': '#9b9f9f',
'check_in': '#e6bd0f',
'check_out': '#09a4cd',
'done': '#315274',
}
COLOR_MNT = {
'draft': '#e87a7a',
'confirmed': '#d45757',
'done': '#d45757',
}
_ZERO = Decimal('0')
class Folio(ModelSQL, ModelView):
'Folio'
__name__ = 'hotel.folio'
STATES = {
'required': Eval('registration_state') == 'check_in',
'readonly': Eval('registration_state') == 'check_in',
}
booking = fields.Many2One('hotel.booking', 'Booking', ondelete='CASCADE',
select=True)
reference = fields.Char('Reference', states=STATES)
registration_card = fields.Char('Registration Card', readonly=True,
select=True, help="Unique sequence for card guest registration.")
room = fields.Many2One('hotel.room', 'Room', select=True,
states=STATES_CHECKIN)
arrival_date = fields.Date('Arrival Date', required=True,
states=STATES_CHECKOUT)
departure_date = fields.Date('Departure Date', required=True,
states=STATES_CHECKOUT)
product = fields.Many2One('product.product', 'Product',
select=True, domain=[
('template.type', '=', 'service'),
('template.kind', '=', 'accommodation'),
], required=True, states=STATES_CHECKOUT)
unit_price = fields.Numeric('Unit Price', digits=(16, 4),
states={
'required': Bool(Eval('product')),
'readonly': Eval('registration_state') == 'check_out',
})
unit_price_w_tax = fields.Function(fields.Numeric('Unit Price'),
'get_unit_price_w_tax')
uom = fields.Many2One('product.uom', 'UOM', readonly=True)
main_guest = fields.Many2One('party.party', 'Main Guest', select=True,
states=STATES_CHECKIN
)
contact = fields.Char('Contact', states=STATES_CHECKIN)
num_children = fields.Function(fields.Integer('No. Children'),
'get_num_children')
nights_quantity = fields.Integer('Nights', states={'readonly': True},
depends=['arrival_date', 'departure_date'])
host_quantity = fields.Integer('Host', states=STATES_CHECKIN)
unit_digits = fields.Function(fields.Integer('Unit Digits'), 'get_unit_digits')
notes = fields.Text('Notes')
total_amount = fields.Function(fields.Numeric('Total Amount',
digits=(16, 2)), 'get_totals')
total_balance = fields.Function(fields.Numeric('Total Balance',
digits=(16, 2)), 'get_totals')
commission_amount = fields.Numeric('Commission Amount', digits=(16, 2),
depends=['nights_quantity', 'unit_price'], states={'readonly': True}
)
guests = fields.One2Many('hotel.folio.guest', 'folio', 'Guests',
states={'readonly': Eval('registration_state').in_(['check_out'])})
nationality = fields.Many2One('party.nationality', 'Nationality',
states=STATES_CHECKIN)
origin_country = fields.Many2One('party.nationality', 'Origin Country',
select=True, states=STATES_CHECKIN)
target_country = fields.Many2One('party.nationality', 'Target Country',
select=True, states=STATES_CHECKIN)
registration_state = fields.Selection(REGISTRATION_STATE,
'Registration State', readonly=True, select=True)
charges = fields.One2Many('hotel.folio.charge', 'folio', 'Charges',
states={
'readonly': ~Eval('registration_state').in_(['check_in']),
})
party = fields.Many2One('party.party', 'Party to Bill', select=True,
states={
'readonly': Eval('registration_state').in_(['check_out']),
})
invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line')
to_invoice = fields.Boolean('To Invoice', states={
'invisible': Bool(Eval('invoice_line')),
}, depends=['invoice_line'], help='Mark this checkbox if you want to invoice this item')
invoice = fields.Function(fields.Many2One('account.invoice', 'Invoice',
depends=['invoice_line']), 'get_invoice')
invoice_state = fields.Function(fields.Selection(
INVOICE_STATES, 'Invoice State'), 'get_invoice_state')
type_complementary = fields.Selection(COMPLEMENTARY, 'Type Complementary',
states={
'invisible': ~Bool(Eval('complementary')),
'required': Bool(Eval('complementary')),
})
breakfast_included = fields.Boolean('Breakfast Included', states={
'readonly': Eval('registration_state') != 'check_in',
})
# paid_by_channel = fields.Boolean('Paid By Channel')
room_amount = fields.Function(fields.Numeric('Room Amount',
digits=(16, 2), depends=['nights_quantity', 'unit_price']
), 'on_change_with_room_amount')
stock_moves = fields.Many2Many('hotel.folio-stock.move', 'folio',
'move', 'Stock Moves', states={'readonly': True})
channel = fields.Function(fields.Many2One('hotel.channel', 'Channel'),
'get_channel')
@classmethod
def __setup__(cls):
super(Folio, cls).__setup__()
cls._check_modify_exclude = [
'nationality', 'origin_country', 'target_country',
'registration_state', 'guests'
],
cls._buttons.update({
'check_in': {
'invisible': Eval('registration_state').in_(
['check_in', 'check_out']
),
},
'check_out': {
'invisible': Eval('registration_state') != 'check_in',
},
'bill': {
'invisible': Eval('registration_state') != 'check_out',
},
'pay': {
'invisible': Eval('registration_state') != 'check_out',
}
})
def get_rec_name(self, name):
name = ''
if self.registration_card:
name = f'{self.registration_card}:'
if self.main_guest:
name = name + self.main_guest.name
return name
@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,
('main_guest', operator, value),
('registration_card', operator, value),
('product.name', operator, value),
('party.name', operator, value),
('reference', operator, value),
]
return domain
@classmethod
def create(cls, values):
folios = super(Folio, cls).create(values)
for folio in folios:
folio.update_nights(folio.arrival_date, folio.departure_date)
folio.update_commission()
folio.save()
@classmethod
@ModelView.button
def check_in(cls, records):
for rec in records:
# rec.booking.party.pre_validate()
if rec.booking.state == 'offer':
raise UserError(gettext('hotel.msg_missing_confirm_booking'))
if rec.main_guest is None:
raise UserError(gettext('hotel.msg_missing_main_guest'))
if rec.room is None:
raise UserError(gettext('hotel.msg_missing_select_room'))
rec.set_registration_number()
rec.booking.check_rooms()
rec.add_main_guest()
cls.write(records, {'registration_state': 'check_in'})
@classmethod
@ModelView.button
def check_out(cls, records):
for record in records:
cls.write([record], {'registration_state': 'check_out'})
@classmethod
@ModelView.button
def bill(cls, records):
for rec in records:
rec.create_invoice()
@classmethod
@ModelView.button_action('hotel.wizard_booking_advance_voucher')
def pay(cls, records):
pass
@staticmethod
def default_to_invoice():
return True
@staticmethod
def default_registration_state():
return 'pending'
def get_invoice_state(self, name=None):
if self.invoice_line:
return self.invoice_line.invoice.state
def get_channel(self, name=None):
if self.booking and self.booking.channel:
return self.booking.channel.id
def set_registration_number(self):
"""
Fill the number field for registration card with sequence
"""
pool = Pool()
Config = pool.get('hotel.configuration')
config = Config.get_configuration()
if self.registration_card:
return
if not config.registration_card_sequence:
raise UserError(gettext('hotel.msg_missing_sequence_registration'))
number = config.registration_card_sequence.get()
self.registration_card = number
self.save()
def add_main_guest(self):
pool = Pool()
Guest = pool.get('hotel.folio.guest')
Guest.create([{
'folio': self.id,
'party': self.main_guest.id,
'type_guest': 'adult',
'type_document': 13,
}])
def create_invoice(self):
pool = Pool()
Booking = pool.get('hotel.booking')
Booking.create_invoice([self])
Booking.check_finished([self.booking])
def get_unit_price_w_tax(self, name=None):
Tax = Pool().get('account.tax')
res = self.unit_price or 0
if self.unit_price:
values = Tax.compute(
self.product.template.customer_taxes_used,
self.unit_price, 1)
if values:
value = values[0]
res = value['base'] + value['amount']
return res
@fields.depends('unit_price', 'nights_quantity', 'arrival_date', 'departure_date')
def on_change_with_room_amount(self, name=None):
res = 0
if self.unit_price and self.nights_quantity:
res = self.unit_price * self.nights_quantity
return round(res, Folio.total_amount.digits[1])
@staticmethod
def default_main_guest():
party = Transaction().context.get('party')
return party
@staticmethod
def default_host_quantity():
return 1
@staticmethod
def default_accommodation():
Configuration = Pool().get('hotel.configuration')
configuration = Configuration.get_configuration()
if configuration.default_accommodation:
return configuration.default_accommodation.id
@staticmethod
def default_breakfast_included():
return True
@classmethod
def validate(cls, lines):
super(Folio, cls).validate(lines)
for line in lines:
# line.check_method()
pass
def get_invoice(self, name=None):
if self.invoice_line and self.invoice_line.invoice:
return self.invoice_line.invoice.id
def get_room_info(self):
description = ' \n'.join([
self.product.rec_name,
'Huesped Principal: ' + self.main_guest.name,
'Habitacion: ' + self.room.name,
'Llegada: ' + str(self.arrival_date),
'Salida: ' + str(self.departure_date),
])
return description
@fields.depends('product', 'unit_price', 'uom', 'booking',
'nights_quantity', 'commission_amount')
def on_change_product(self):
Product = Pool().get('product.product')
if self.product and self.booking:
self.uom = self.product.default_uom.id
with Transaction().set_context(
self.booking.get_context_price(self.product)):
self.unit_price = Product.get_sale_price([self.product],
self.nights_quantity or 0)[self.product.id]
if self.unit_price:
self.unit_price = self.booking.currency.round(self.unit_price)
else:
self.unit_price = self.product.list_price
self.update_commission()
def update_commission(self):
if self.nights_quantity and self.unit_price and self.booking:
if not self.booking.channel:
return
amount = self.on_change_with_room_amount()
self.commission_amount = self.booking.channel.compute(amount)
@fields.depends('arrival_date', 'departure_date', 'unit_price', 'booking')
def on_change_arrival_date(self):
if self.arrival_date and self.departure_date:
self.update_nights(self.arrival_date, self.departure_date)
self.update_commission()
@fields.depends('arrival_date', 'departure_date', 'unit_price', 'booking')
def on_change_departure_date(self):
if self.arrival_date and self.departure_date:
self.update_nights(self.arrival_date, self.departure_date)
self.update_commission()
def check_method(self):
"""
Check the methods.
"""
Date = Pool().get('ir.date')
if self.registration_state in (['check_in', 'check_out']):
raise UserError(gettext('hotel.msg_reservation_checkin'))
if self.arrival_date < Date.today():
raise UserError(gettext('hotel.msg_invalid_arrival_date'))
if self.arrival_date >= self.departure_date:
raise UserError(gettext('hotel.msg_invalid_date'))
def get_state(self, name):
if self.booking:
return self.booking.state
def get_host_quantity(self, name):
res = 0
if self.num_adult:
res += self.num_adult
if self.num_children:
res += self.num_children
return res
@classmethod
def get_available_rooms(cls, start_date, end_date, rooms_ids=[], oper=None):
"""
Look for available rooms.
given the date interval, return a list of room ids.
a room is available if it has no operation that overlaps
with the given date interval.
the optional 'rooms' list is a list of room instance, is an
additional filter, specifying the ids of the desirable rooms.
the optional 'oper' is an operation object that has to be
filtered out of the test. it is useful for validating an already
existing operation.
"""
if start_date >= end_date:
raise UserError(gettext('hotel.msg_invalid_date_range'))
# define the domain of the operations that find a
# room to be available
dom = ['AND', ['OR',
[
('arrival_date', '>=', start_date),
('arrival_date', '<', end_date),
], [
('departure_date', '<=', end_date),
('departure_date', '>', start_date),
], [
('arrival_date', '<=', start_date),
('departure_date', '>=', end_date),
],
]]
## If oper was specified, do not compare the operations with it
if oper is not None:
dom.append(('id', '!=', oper.id))
if rooms_ids:
dom.append(('room', 'in', rooms_ids))
folios = cls.search(dom)
rooms_not_available_ids = [folio.room.id for folio in folios]
rooms_available_ids = set(rooms_ids) - set(rooms_not_available_ids)
return list(rooms_available_ids)
def update_nights(self, arrival_date, departure_date):
self.nights_quantity = (departure_date - arrival_date).days
def get_totals(self, name):
"""
The total amount of booking based on room flat price.
TODO: If room fee is applied should be used for total price calculation
instead of flat price. Fee is linked to channel management.
"""
res = []
amount_nights = 0
if self.nights_quantity and self.unit_price_w_tax:
amount_nights = self.nights_quantity * self.unit_price_w_tax
if name == 'total_amount' or (name == 'total_balance' and not self.invoice_line):
res.append(amount_nights)
for charge in self.charges:
res.append(charge.amount)
res = round(sum(res), Folio.total_amount.digits[1])
return res
@fields.depends('nights_quantity', 'unit_price', 'booking')
def on_change_with_commission_amount(self):
"""
Calculation of commission amount for channel based on booking
"""
res = Decimal(0)
if self.nights_quantity and self.unit_price and self.booking.channel:
amount = self.on_change_with_room_amount()
res = self.booking.channel.compute(amount)
return res
class FolioGuest(ModelSQL, ModelView):
'Folio Guest'
__name__ = 'hotel.folio.guest'
_rec_name = 'party'
folio = fields.Many2One('hotel.folio', 'Folio', required=True,
ondelete='CASCADE')
party = fields.Many2One('party.party', 'Party', required=False)
type_guest = fields.Selection([
('adult', 'Adult'),
('child', 'Child'),
], 'Type Guest')
type_guest_string = type_guest.translated('type_guest')
nationality = fields.Many2One('party.nationality', 'Nationality')
origin_country = fields.Many2One('party.nationality', 'Origin Country',
select=True)
target_country = fields.Many2One('party.nationality', 'Target Country',
select=True)
# New fields for speed reason
type_document = fields.Selection([
('12', 'Tarjeta de Identidad'),
('13', 'Cedula de Ciudadania'),
('21', 'Tarjeta de Extranjeria'),
('22', 'Cedula de Extranjeria'),
('41', 'Pasaporte'),
], 'Document Type')
doc_number = fields.Char('Doc. Id', select=True)
name = fields.Char('Name', select=True)
mobile = fields.Char('Mobile', select=True)
email = fields.Char('Email', select=True)
birthday = fields.Date('Birthday', select=True)
sex = fields.Selection([
('female', 'Female'),
('male', 'Male'),
('', ''),
], 'Sex')
first_name = fields.Char('First Name')
second_name = fields.Char('Second Name')
first_family_name = fields.Char('First Family Name')
second_family_name = fields.Char('Second Family Name')
type_person = fields.Selection([
('persona_natural', 'Persona Natural'),
('persona_juridica', 'Persona Juridica'),
], 'Type Person')
@fields.depends('name', 'first_name', 'second_name',
'first_family_name', 'second_family_name', 'type_person')
def on_change_name(self):
second_family_name = None
first_family_name = None
second_name = None
first_name = None
if self.name and self.type_person == 'persona_natural':
names = self.name.split(' ')
first_name = names[0]
second_family_name = names[-1]
if len(names) > 1:
first_family_name = names[-2]
if len(names) == 2:
second_family_name = None
first_family_name = names[1]
elif len(names) == 5:
second_name = names[1] + ' ' + names[2]
elif len(names) == 4:
second_name = names[1]
self.second_family_name = second_family_name
self.first_family_name = first_family_name
self.second_name = second_name
self.first_name = first_name
def get_rec_name(self, name):
if self.party:
return self.party.name
@staticmethod
def default_type_guest():
return 'adult'
@staticmethod
def default_sex():
return 'male'
@staticmethod
def default_type_person():
return 'persona_natural'
@staticmethod
def default_type_document():
return '13'
@staticmethod
def default_principal_guest():
return False
@fields.depends('nationality', 'origin_country', 'target_country')
def on_change_nationality(self):
if self.nationality:
self.target_country = self.nationality.id
self.origin_country = self.nationality.id
@classmethod
def create(cls, vlist):
Party = Pool().get('party.party')
new_values = []
for v in vlist:
# if not v.get('doc_number'):
# continue
party = v.get('party')
if not party:
parties = Party.search([
('id_number', '=', v['doc_number']),
])
if parties:
party = parties[0]
else:
party, = Party.create([{
'name': v['name'],
'id_number': v['doc_number'],
'type_document': v['type_document'],
'birthday': v['birthday'],
'sex': v['sex'],
'first_name': v['first_name'],
'second_name': v['second_name'],
'first_family_name': v['first_family_name'],
'second_family_name': v['second_family_name'],
'type_person': v['type_person'],
'contact_mechanisms': [
('create', [
{'type': 'email', 'value': v['email']},
{'type': 'mobile', 'value': v['mobile']},
])
]
}])
v['party'] = party.id
new_values.append(v)
super(FolioGuest, cls).create(new_values)
class OperationMaintenance(Workflow, ModelSQL, ModelView):
'Operation Maintenance'
__name__ = 'hotel.operation.maintenance'
operation = fields.Many2One('hotel.operation', 'Operation', states=STATES_MNT)
room = fields.Many2One('hotel.room', 'Room', states=STATES_MNT, select=True,
required=True)
kind = fields.Selection([
('maintenance', 'Maintenance'),
], 'Kind', readonly=True, select=True)
start_date = fields.Date('Start Date', states=STATES_MNT, required=True,
select=True)
end_date = fields.Date('End Date', states=STATES_MNT, required=True,
select=True)
description = fields.Text('Description', states=STATES_MNT, select=True)
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('finished', 'Finished'),
('cancelled', 'Canceled'),
], 'State', readonly=True, select=True)
@classmethod
def __setup__(cls):
super(OperationMaintenance, cls).__setup__()
cls._transitions |= set((
('draft', 'confirmed'),
('draft', 'cancelled'),
('confirmed', 'finished'),
('confirmed', 'cancelled'),
))
cls._buttons.update({
'cancel': {
'invisible': ~Eval('state').in_(['draft', 'confirmed']),
},
'draft': {
'invisible': Eval('state') != 'confirmed',
},
'confirm': {
'invisible': Eval('state') != 'draft',
},
'finish': {
'invisible': Eval('state') != 'confirmed',
},
})
@staticmethod
def default_kind():
return 'maintenance'
@staticmethod
def default_state():
return 'draft'
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, records):
for mant in records:
mant.delete_operation()
@classmethod
@ModelView.button
@Workflow.transition('confirmed')
def confirm(cls, records):
for mant in records:
pass
@classmethod
@ModelView.button
@Workflow.transition('finished')
def finish(cls, records):
for mant in records:
pass
@classmethod
def write(cls, records, values):
Room = Pool().get('hotel.room')
for rec in records:
if values.get('start_date') or values.get('end_date') or values.get('room'):
start_date = values.get('start_date') or rec.start_date
end_date = values.get('end_date') or rec.end_date
room = Room(values.get('room') or rec.room)
# Operation().check_dates(start_date, end_date, room, rec.operation)
# rec.update_operation({
# 'start_date': start_date,
# 'end_date': end_date,
# 'room': room.id,
# })
super(OperationMaintenance, cls).write(records, values)
def check_method(self):
"""
Check the methods.
"""
Operation = Pool().get('hotel.operation')
Operation().check_dates(self.start_date, self.end_date, self.room,
self.operation)
class FolioCharge(Workflow, ModelSQL, ModelView):
'Folio Charge'
__name__ = 'hotel.folio.charge'
folio = fields.Many2One('hotel.folio', 'Folio', required=True)
date_service = fields.Date('Date Service', select=True, required=True)
product = fields.Many2One('product.product', 'Product',
domain=[('salable', '=', True)], required=True)
quantity = fields.Integer('Quantity', required=True)
invoice_to = fields.Many2One('party.party', 'Invoice To', required=True)
unit_price = fields.Numeric('Unit Price', digits=(16, 4),
required=True)
unit_price_w_tax = fields.Function(fields.Numeric('Unit Price'),
'get_unit_price_w_tax')
order = fields.Char('Order', select=True)
description = fields.Char('Description', select=True)
state = fields.Selection(INVOICE_STATES, 'State', readonly=True)
state_string = state.translated('state')
invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line',
readonly=True)
amount = fields.Function(fields.Numeric('Amount',
digits=(16, 2)), 'get_amount')
taxed_amount = fields.Function(fields.Numeric('Amount with Tax',
digits=(16, 2)), 'get_taxed_amount')
to_invoice = fields.Boolean('To Invoice', states={
'invisible': Bool(Eval('invoice_line')),
}, depends=['invoice_line'])
@classmethod
def __setup__(cls):
super(FolioCharge, cls).__setup__()
cls._buttons.update({
'transfer': {
'invisible': True,
},
'bill': {
'invisible': Eval('invoice_state') is not None,
},
})
@staticmethod
def default_quantity():
return 1
@staticmethod
def default_to_invoice():
return True
@staticmethod
def default_date_service():
today = Pool().get('ir.date').today()
return today
def get_amount(self, name=None):
if self.quantity and self.unit_price:
return self.quantity * self.unit_price_w_tax
return 0
def get_unit_price_w_tax(self, name=None):
Tax = Pool().get('account.tax')
res = self.unit_price or 0
if self.unit_price:
values = Tax.compute(self.product.template.customer_taxes_used,
self.unit_price, 1)
if values:
value = values[0]
res = value['base'] + value['amount']
return res
def get_taxed_amount(self, name=None):
if self.quantity and self.unit_price:
return self.quantity * self.unit_price
# def get_sale(self, name=None):
# if self.sale_line:
# return self.sale_line.sale.id
# def compute_amount_with_tax(line):
# tax_amount = _ZERO
# amount = _ZERO
# if line.taxes:
# tax_list = Tax.compute(line.taxes, line.unit_price or _ZERO,
# line.quantity or 0.0)
#
# tax_amount = sum([t['amount'] for t in tax_list], _ZERO)
#
# if line.unit_price:
# amount = line.unit_price * Decimal(line.quantity)
# return amount + tax_amount
@classmethod
@ModelView.button
def bill(cls, records):
cls.create_sales(records)
@classmethod
@ModelView.button_action('hotel.wizard_operation_line_transfer')
def transfer(cls, records):
pass
@fields.depends('unit_price', 'product')
def on_change_product(self):
if self.product:
self.unit_price = self.product.template.list_price
self.description = self.product.description
class OpenMigrationStart(ModelView):
'Open Migration Start'
__name__ = 'hotel.open_migration.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 OpenMigration(Wizard):
'Open Migration'
__name__ = 'hotel.open_migration'
start = StateView('hotel.open_migration.start',
'hotel.open_migration_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Open', 'print_', 'tryton-print', default=True),
])
print_ = StateAction('hotel.report_migration')
def do_print_(self, action):
data = {
'start_date': self.start.start_date,
'end_date': self.start.end_date,
'company': self.start.company.id,
}
return action, data
def transition_print_(self):
return 'end'
class Migration(Report):
'Hotel Migration'
__name__ = 'hotel.migration'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
Folio = Pool().get('hotel.folio')
start = data['start_date']
end = data['end_date']
report_context['records'] = Folio.search([
('arrival_date', '>=', start),
('arrival_date', '<=', end),
('main_guest', '!=', None),
('guests.party', '!=', None),
('registration_state', 'in', ['check_in', 'check_out']),
])
return report_context
class OperationReport(Report):
__name__ = 'hotel.operation'
@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 CheckOutOperationFailed(ModelView):
'Check Out Operation Failed'
__name__ = 'hotel.operation.check_out.failed'
class CheckOutOperation(Wizard):
'Check Out Operation'
__name__ = 'hotel.operation.check_out'
start = StateTransition()
failed = StateView('hotel.operation.check_out.failed',
'hotel.operation_check_out_failed_view_form', [
Button('Force Check Out', 'force', 'tryton-forward'),
Button('Cancel', 'end', 'tryton-cancel', True),
])
force = StateTransition()
def transition_start(self):
Operation = Pool().get('hotel.operation')
active_id = Transaction().context['active_id']
operations = Operation.browse([active_id])
if Operation.validate_check_out_date(operations):
return 'end'
else:
return 'failed'
def transition_force(self):
Operation = Pool().get('hotel.operation')
active_id = Transaction().context['active_id']
operations = Operation.browse([active_id])
Operation.check_out(operations)
return 'end'
class ChangeRoomStart(ModelView):
'Change Room'
__name__ = 'hotel.operation.change_room.ask'
from_date = fields.Date('From Date', required=True)
room = fields.Many2One('hotel.room', 'Room', required=True, domain=[
# ('id', 'in', Eval('targets')),
])
accommodation = fields.Many2One('product.product',
'Accommodation', domain=[
('template.kind', '=', 'accommodation'),
], required=True)
targets = fields.Function(fields.Many2Many('hotel.room', None, None,
'Targets'), 'on_change_with_targets')
tranfer_charges = fields.Boolean('Transfer Charges')
@staticmethod
def default_from_date():
today = Pool().get('ir.date').today()
return today
@fields.depends('from_date', 'accommodation')
def on_change_with_targets(self, name=None):
pool = Pool()
Operation = pool.get('hotel.operation')
RoomTemplate = pool.get('hotel.room-product.template')
operation = Operation(Transaction().context.get('active_id'))
res = []
if not self.accommodation or not self.from_date:
return res
room_templates = RoomTemplate.search([
('template.accommodation_capacity', '>=', self.accommodation.accommodation_capacity)
])
rooms_ids = [t.room.id for t in room_templates]
rooms_available_ids = Operation.get_available_rooms(
self.from_date,
operation.end_date,
rooms_ids=rooms_ids
)
return rooms_available_ids
class ChangeRoom(Wizard):
'Change Room'
__name__ = 'hotel.folio.change_room'
"""
this is the wizard that allows the front desk employee to transfer
original room, and create a new operation occupany.
"""
start = StateView('hotel.folio.change_room.ask',
'hotel.folio_change_room_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Change', 'change', 'tryton-ok'),
]
)
change = StateTransition()
def transition_change(self):
pool = Pool()
Folio = pool.get('hotel.folio')
FolioCharge = pool.get('hotel.folio.charge')
operation = Folio(Transaction().context.get('active_id'))
new_operation = {
'reference': operation.reference,
'room': self.start.room.id,
'start_date': self.start.from_date,
'end_date': operation.end_date,
'party': operation.party.id,
'currency': operation.currency.id,
'company': operation.company.id,
'kind': operation.kind,
'main_guest': operation.main_guest.id,
'accommodation': self.start.accommodation.id,
'origin': str(operation.origin),
'state': 'open',
'unit_price': operation.unit_price,
'lines': []
}
lines_to_transfer = []
# _operation, = Operation.create([new_operation])
# if self.start.tranfer_charges:
# for line in operation.lines:
# if line.state is None:
# lines_to_transfer.append(line)
#
# if lines_to_transfer:
# FolioCharge.write([lines_to_transfer], {'operation': _operation.id})
#
# operation.end_date = self.start.from_date
# operation.state = 'closed'
# operation.target = _operation.id
# operation.save()
return 'end'
class TransferOperationStart(ModelView):
'Transfer Operation'
__name__ = 'hotel.folio.transfer_operation.ask'
folio = fields.Many2One('hotel.folio', 'Folio',
required=True, domain=[
('state', 'in', ['draft', 'open']),
])
tranfer_charges = fields.Boolean('Transfer Charges')
@staticmethod
def default_tranfer_charges():
return True
class TransferOperation(Wizard):
'Transfer Operation'
__name__ = 'hotel.folio.transfer_operation'
"""
this is the wizard that allows the front desk employee to transfer
original room, and create a new operation occupany.
"""
start = StateView('hotel.folio.transfer_operation.ask',
'hotel.folio_transfer_operation_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Transfer', 'transfer', 'tryton-ok'),
]
)
transfer = StateTransition()
@classmethod
def __setup__(cls):
super(TransferOperation, cls).__setup__()
def transition_transfer(self):
pool = Pool()
Operation = pool.get('hotel.folio')
FolioCharge = pool.get('hotel.folio.charge')
current_op = Operation(Transaction().context.get('active_id'))
target_op = self.start.folio
if target_op.id == current_op.id:
raise AccessError(gettext('hotel.msg_folio_current'))
lines_to_transfer = []
if self.start.tranfer_charges:
for line in current_op.lines:
if line.state is None:
lines_to_transfer.append(line)
if lines_to_transfer:
FolioCharge.write(lines_to_transfer, {
'operation': target_op.id
})
current_op.state = 'transfered'
current_op.operation_target = target_op.id
current_op.save()
target_op.save()
return 'end'
class TransferChargeStart(ModelView):
'Transfer Charge'
__name__ = 'hotel.folio.transfer_charge.ask'
folio = fields.Many2One('hotel.folio', 'Operation',
required=True, domain=[('state', '=', 'check_in')])
class TransferCharge(Wizard):
'Transfer Operation'
__name__ = 'hotel.folio.transfer_charge'
start = StateView('hotel.folio.transfer_charge.ask',
'hotel.folio_transfer_charge_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Transfer', 'transfer', 'tryton-ok'),
]
)
transfer = StateTransition()
def transition_transfer(self):
pool = Pool()
FolioCharge = pool.get('hotel.folio.charge')
current_line = FolioCharge(Transaction().context.get('active_id'))
target_op = self.start.operation
current_operation = current_line.operation
current_line.operation = target_op.id
current_line.save()
current_operation.save()
return 'end'
class OperationByConsumerReport(Report):
__name__ = 'hotel.folio.charge'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
Folio = pool.get('hotel.folio')
user = pool.get('res.user')(Transaction().user)
consumer_lines = []
total_amount = 0
if records:
line = records[0]
folio = Folio(line.folio.id)
total_amount = folio.room_amount
for l in folio.lines:
if l.invoice_to.id == line.invoice_to.id:
consumer_lines.append(l)
total_amount += l.amount
setattr(folio, 'lines', consumer_lines)
setattr(folio, 'total_amount', total_amount)
report_context['records'] = [folio]
report_context['company'] = user.company
return report_context
class OperationBill(Wizard):
'Operation Bill'
__name__ = 'hotel.folio.bill'
start_state = 'create_bill'
create_bill = StateTransition()
def transition_create_bill(self):
Folio = Pool().get('hotel.folio')
ids = Transaction().context['active_ids']
folios = Folio.browse(ids)
Folio.bill(folios)
return 'end'
class OperationVoucher(ModelSQL):
'Operation - Voucher'
__name__ = 'hotel.folio-account.voucher'
_table = 'folio_vouchers_rel'
folio = fields.Many2One('hotel.folio', 'Folio',
ondelete='CASCADE', required=True)
voucher = fields.Many2One('account.voucher', 'Voucher', required=True,
domain=[('voucher_type', '=', 'receipt')], ondelete='RESTRICT')
@classmethod
def set_voucher_origin(cls, voucher_id, folio_id):
cls.create([{
'voucher': voucher_id,
'folio': folio_id,
}])
class ReverseCheckout(Wizard):
'Reverse Checkout'
__name__ = 'hotel.folio.reverse_checkout'
start_state = 'reverse_checkout'
reverse_checkout = StateTransition()
def transition_reverse_checkout(self):
Folio = Pool().get('hotel.folio')
folio_id = Transaction().context['active_id']
if folio_id:
Folio.check_in([Folio(folio_id)])
return 'end'
class FolioStockMove(ModelSQL):
'Folio - Stock Move'
__name__ = 'hotel.folio-stock.move'
_table = 'hotel_folio_stock_move_rel'
folio = fields.Many2One('hotel.folio', 'Folio',
ondelete='CASCADE', select=True, required=True)
move = fields.Many2One('stock.move', 'Move', select=True,
ondelete='RESTRICT', required=True)
class StatisticsByMonthStart(ModelView):
'Statistics By Month Start'
__name__ = 'hotel.statistics_by_month.start'
period = fields.Many2One('account.period', 'Period', required=True)
company = fields.Many2One('company.company', 'Company', required=True)
@staticmethod
def default_company():
return Transaction().context.get('company')
class StatisticsByMonth(Wizard):
'Statistics By Month'
__name__ = 'hotel.statistics_by_month'
start = StateView('hotel.statistics_by_month.start',
'hotel.print_hotel_statistics_by_month_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Open', 'print_', 'tryton-print', default=True),
])
print_ = StateAction('hotel.statistics_by_month_report')
def do_print_(self, action):
data = {
'period': self.start.period.id,
'company': self.start.company.id,
}
return action, data
def transition_print_(self):
return 'end'
class StatisticsByMonthReport(Report):
'Hotel Statistics By Month'
__name__ = 'hotel.statistics_by_month.report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
Operation = Pool().get('hotel.folio')
Period = Pool().get('account.period')
Company = Pool().get('company.company')
Rooms = Pool().get('hotel.room')
period = Period(data['period'])
company = Company(data['company'])
month_days = (period.end_date - period.start_date).days + 1
rooms_available = []
rooms_sold = []
beds_available = []
beds_sold = []
local_guests = []
foreign_guests = []
room1_sold = []
room2_sold = []
room_suite_sold = []
room_other_sold = []
guests_one_ng = []
guests_two_ng = []
guests_three_ng = []
guests_four_ng = []
guests_five_ng = []
guests_six_ng = []
guests_seven_ng = []
guests_eight_ng = []
guests_permanent_ng = []
reason_bussiness = []
reason_bussiness_foreign = []
reason_leisure = []
reason_leisure_foreign = []
reason_convention = []
reason_convention_foreign = []
reason_health = []
reason_health_foreign = []
reason_transport = []
reason_transport_foreign = []
reason_other = []
reason_other_foreign = []
total_guests = []
folios = Operation.search(['OR',
[
('start_date', '>=', period.start_date),
('start_date', '<=', period.end_date),
('kind', '=', 'occupancy'),
], [
('end_date', '>=', period.start_date),
('end_date', '<=', period.end_date),
('kind', '=', 'occupancy'),
]
])
rooms = Rooms.search([])
rooms_available = [len(rooms) * month_days]
for r in rooms:
beds_available.append(
r.main_accommodation.accommodation_capacity * month_days
)
def _get_ctx_dates(op):
start_date = op.start_date
end_date = op.end_date
if op.start_date < period.start_date:
start_date = period.start_date
if op.end_date > period.end_date:
end_date = period.end_date
sub_dates = [
str(start_date + timedelta(n))
for n in range((end_date - start_date).days)
]
return sub_dates
for op in folios:
room_capacity = op.room.main_accommodation.accommodation_capacity
ctx_dates = _get_ctx_dates(op)
len_ctx_dates = len(ctx_dates)
rooms_sold.append(len_ctx_dates)
beds_sold.append(len_ctx_dates * room_capacity)
qty_guests = len(op.guests)
if not qty_guests:
qty_guests = 1
total_guests.append(qty_guests)
is_local = True
if op.main_guest.type_document == '41':
is_local = False
foreign_guests.append(qty_guests)
else:
local_guests.append(qty_guests)
if room_capacity == 1:
room1_sold.append(qty_guests)
elif room_capacity == 2:
room2_sold.append(qty_guests)
elif room_capacity > 2:
room_suite_sold.append(qty_guests)
if len_ctx_dates == 1:
guests_one_ng.append(qty_guests)
elif len_ctx_dates == 2:
guests_two_ng.append(qty_guests)
elif len_ctx_dates == 3:
guests_three_ng.append(qty_guests)
elif len_ctx_dates == 4:
guests_four_ng.append(qty_guests)
elif len_ctx_dates == 5:
guests_five_ng.append(qty_guests)
elif len_ctx_dates == 6:
guests_six_ng.append(qty_guests)
elif len_ctx_dates == 7:
guests_seven_ng.append(qty_guests)
elif len_ctx_dates >= 8:
guests_eight_ng.append(qty_guests)
segment = 'bussiness'
if op.origin:
if hasattr(op.origin, 'booking'):
segment = op.origin.booking.segment
if segment == 'bussiness':
if is_local:
reason_bussiness.append(qty_guests)
else:
reason_bussiness_foreign.append(qty_guests)
elif segment == 'convention':
if is_local:
reason_convention.append(qty_guests)
else:
reason_convention_foreign.append(qty_guests)
elif segment == 'health':
if is_local:
reason_health.append(qty_guests)
else:
reason_health_foreign.append(qty_guests)
else:
if is_local:
reason_leisure.append(qty_guests)
else:
reason_leisure_foreign.append(qty_guests)
def _get_rate(val):
res = 0
if sum_total_guests > 0:
res = round(float(sum(val)) / sum_total_guests, 4)
return res
sum_total_guests = sum(total_guests)
rate_guests_one_ng = _get_rate(guests_one_ng)
rate_guests_two_ng = _get_rate(guests_two_ng)
rate_guests_three_ng = _get_rate(guests_three_ng)
rate_guests_four_ng = _get_rate(guests_four_ng)
rate_guests_five_ng = _get_rate(guests_five_ng)
rate_guests_six_ng = _get_rate(guests_six_ng)
rate_guests_seven_ng = _get_rate(guests_seven_ng)
rate_guests_eight_ng = _get_rate(guests_eight_ng)
report_context['period'] = period.name
report_context['company'] = company.party.name
report_context['rooms_available'] = sum(rooms_available)
report_context['rooms_sold'] = sum(rooms_sold)
report_context['beds_available'] = sum(beds_available)
report_context['beds_sold'] = sum(beds_sold)
report_context['local_guests'] = sum(local_guests)
report_context['foreign_guests'] = sum(foreign_guests)
report_context['room1_sold'] = sum(room1_sold)
report_context['room2_sold'] = sum(room2_sold)
report_context['room_suite_sold'] = sum(room_suite_sold)
report_context['room_other_sold'] = sum(room_other_sold)
report_context['guests_one_ng'] = rate_guests_one_ng
report_context['guests_two_ng'] = rate_guests_two_ng
report_context['guests_three_ng'] = rate_guests_three_ng
report_context['guests_four_ng'] = rate_guests_four_ng
report_context['guests_five_ng'] = rate_guests_five_ng
report_context['guests_six_ng'] = rate_guests_six_ng
report_context['guests_seven_ng'] = rate_guests_seven_ng
report_context['guests_eight_ng'] = rate_guests_eight_ng
report_context['guests_permanent_ng'] = sum(guests_permanent_ng)
report_context['reason_bussiness'] = sum(reason_bussiness)
report_context['reason_bussiness_foreign'] = sum(reason_bussiness_foreign)
report_context['reason_leisure'] = sum(reason_leisure)
report_context['reason_leisure_foreign'] = sum(reason_leisure_foreign)
report_context['reason_convention'] = sum(reason_convention)
report_context['reason_convention_foreign'] = sum(reason_convention_foreign)
report_context['reason_health'] = sum(reason_health)
report_context['reason_health_foreign'] = sum(reason_health_foreign)
report_context['reason_transport'] = sum(reason_transport)
report_context['reason_transport_foreign'] = sum(reason_transport_foreign)
report_context['reason_other_foreign'] = sum(reason_other_foreign)
report_context['reason_other'] = sum(reason_other)
return report_context
class GuestsListStart(ModelView):
'Guests List Start'
__name__ = 'hotel.print_guests_list.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 GuestsList(Wizard):
'Guests List'
__name__ = 'hotel.print_guests_list'
start = StateView('hotel.print_guests_list.start',
'hotel.print_guests_list_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Open', 'print_', 'tryton-print', default=True),
])
print_ = StateReport('hotel.guests_list.report')
def do_print_(self, action):
company = self.start.company
data = {
'date': self.start.date,
'company': company.id,
}
return action, data
def transition_print_(self):
return 'end'
class GuestsListReport(Report):
__name__ = 'hotel.guests_list.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')
Room = pool.get('hotel.room')
User = pool.get('res.user')
user_id = Transaction().user
user = User(user_id)
# start_date = data['date']
if data['date'] == date.today():
dom = ['OR', [
('arrival_date', '=', data['date']),
('registration_state', '=', 'check_in'),
], [
('departure_date', '=', data['date']),
('registration_state', '=', 'check_in'),
], [
('arrival_date', '<=', data['date']),
('departure_date', '>=', data['date']),
('registration_state', '=', 'check_in'),
]
]
else:
dom = [
('arrival_date', '<=', data['date']),
('departure_date', '>=', data['date']),
]
folios = Folio.search(dom, order=[('room.code', 'ASC')])
total_guests = []
rooms_occupied = []
for op in folios:
total_guests.append(len(op.guests))
rooms_occupied.append(op.room.id)
rooms = Room.search_read([])
_rooms_occupied = len(set(rooms_occupied))
num_rooms = len(rooms)
if num_rooms:
occupancy_rate = round(_rooms_occupied / num_rooms, 2)
else:
occupancy_rate = 0
guests = []
for op in folios:
for guest in op.guests:
guests.append({
'room': op.room.code,
'guest': guest.party.name,
'party': op.party.name if op.party else '',
'arrival_date': op.arrival_date,
'departure_date': op.departure_date,
'nights_quantity': op.nights_quantity,
})
report_context['records'] = guests
report_context['total_rooms'] = _rooms_occupied
report_context['total_guests'] = sum(total_guests)
report_context['occupancy_rate'] = occupancy_rate
report_context['company'] = Company(data['company']).party.name
report_context['date'] = data['date']
report_context['print_date'] = datetime.now()
report_context['user'] = user.name
return report_context
class RegistrationCardReport(Report):
__name__ = 'hotel.folio.registration_card'
@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
_records = []
for rec in records:
if not rec.registration_card:
continue
_records.append(rec)
report_context['records'] = _records
return report_context