# 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, COLOR_BOOKING, COLOR_MNT) 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'} _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={ 'readonly': True } ) 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) estimated_arrival_time = fields.Time('Estimated Arrival Time', 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'])}) storage = fields.Many2One('stock.location', 'Storage', domain=[('type', '=', 'storage')], states={ 'readonly': Eval('registration_state') != 'check_in', }, depends=['registration_state']) registration_state = fields.Selection(REGISTRATION_STATE, 'Registration State', readonly=True, select=True) registration_state_string = registration_state.translated('registration_state') 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', }) room_amount = fields.Function(fields.Numeric('Room Amount', digits=(16, 2), depends=['nights_quantity', 'unit_price'] ), 'on_change_with_room_amount') channel = fields.Function(fields.Many2One('hotel.channel', 'Channel'), 'get_channel') num_children = fields.Function(fields.Integer('Num. Children'), 'get_num_guests') num_adults = fields.Function(fields.Integer('Num. Adults'), 'get_num_guests') stock_moves = fields.Function(fields.One2Many('stock.move', 'origin', 'Moves', readonly=True), 'get_stock_moves') vehicle_plate = fields.Char('Vehicle Plate') taxes = fields.Many2Many('hotel.folio-account.tax', 'folio', 'tax', 'Taxes', order=[('tax.sequence', 'ASC'), ('tax.id', 'ASC')], domain=[('parent', '=', None), ['OR', ('group', '=', None), ('group.kind', 'in', ['sale', 'both'])], ], ) @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_num_guests(self, name): res = [] for guest in self.guests: if name == 'num_children' and guest.type_guest == 'child': res.append(1) elif name == 'num_adults' and guest.type_guest == 'adult': res.append(1) return sum(res) def get_rec_name(self, name=None): if self.room: name = f'{self.room.code}:' 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.check_room() cls.update_room(rec, 'check_in') cls.write(records, {'registration_state': 'check_in'}) def check_room(self): if self.room.state != 'clean': raise UserError( gettext('hotel.msg_room_no_clean', s=self.room.name) ) # def get_tax_amount(self): # return sum( # (v['amount'] for v in self._get_taxes().values()), Decimal(0)) @classmethod @ModelView.button def check_out(cls, records): for record in records: cls.write([record], {'registration_state': 'check_out'}) cls.update_room(record, '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' @classmethod def update_room(cls, folio, status): pool = Pool() Configuration = pool.get('hotel.configuration') config = Configuration.get_configuration() cleaning_type_id = None room = folio.room if status in 'check_in' and config.cleaning_occupied: cleaning_type_id = config.cleaning_occupied.id room.last_check_in = datetime.now() room.last_check_out = None elif status in 'check_out' and config.cleaning_check_out: cleaning_type_id = config.cleaning_check_out.id room.last_check_out = datetime.now() room.last_check_in = None room.state = 'dirty' if cleaning_type_id: room.cleaning_type = cleaning_type_id room.save() 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 get_stock_moves(self, name=None): moves = [] for charge in self.charges: if charge.move: moves.append(charge.move.id) return moves 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 if self.main_guest else '', '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. """ Maintenance = Pool().get('hotel.maintenance') 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] dom = ['AND', ['OR', [ ('start_date', '>=', start_date), ('start_date', '<=', end_date), ], [ ('end_date', '<=', end_date), ('end_date', '>=', start_date), ], [ ('start_date', '<=', start_date), ('end_date', '>=', end_date), ], ], ('state', 'in', ('in_progress', 'finished')), ] maintenance = Maintenance.search(dom) rooms_not_available_ids.extend(mnt.room.id for mnt in maintenance) 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 @classmethod def get_folios(cls, args): Maintenance = Pool().get('hotel.maintenance') start = datetime.strptime(args['start_date'], "%Y-%m-%d") start_date = start + timedelta(days=-5) end_date = start + timedelta(days=30) dom = ['OR', [ ('arrival_date', '>=', start_date), ('arrival_date', '<=', end_date) ], [ ('departure_date', '>=', start_date), ('departure_date', '<=', end_date) ]] fields_names = [ "room.name", "arrival_date", "departure_date", "main_guest.name", "product.rec_name", "booking.number", "booking.media", "booking.channel.rec_name", "booking.channel.code", "registration_state", "registration_card", "nights_quantity", "notes", ] folios = cls.search_read(dom, fields_names=fields_names) dom_maint = ['OR', [ ('start_date', '>=', start_date), ('start_date', '<=', end_date), ('state', 'in', ('in_progress', 'finished', 'confirmed')), ('start_date', '!=', None), ('end_date', '!=', None), ], [ ('end_date', '>=', start_date), ('end_date', '<=', end_date), ('state', 'in', ('in_progress', 'finished', 'confirmed')), ('start_date', '!=', None), ('end_date', '!=', None), ], ] fields_maint = [ 'start_date', 'end_date', 'room', 'room.name', 'issue', 'create_uid.name' ] mnts = Maintenance.search_read(dom_maint, fields_names=fields_maint) return {'folios': folios, 'mnts': mnts} 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 @classmethod def _get_check_rooms(cls, kind): # Dash Report # kind : arrival or departure field_target = kind + '_date' folios = cls.search_read([ (field_target, '=', date.today()) ], fields_names=['id']) return len(folios) @classmethod def report_check_in_today(cls, args): # Dash Report value = cls._get_check_rooms('arrival') res = { 'value': value, 'description': '', 'selector': {}, 'default_option': None, 'meta': 'prueba 1', 'in_thousands': None } return res @classmethod def report_check_out_today(cls, args): # Dash Report value = cls._get_check_rooms('departure') res = { 'value': value, 'description': '', 'selector': {}, 'default_option': None, 'meta': 'prueba 1', 'in_thousands': None } return res @classmethod def _get_occupation(cls): today = date.today() dom = [ ('arrival_date', '<=', today), ('departure_date', '>', today), ] folios = cls.search_read(dom, fields_names=['id', 'guests']) return folios @classmethod def report_occupation_by_room(cls, args): # Dash Report folios = cls._get_occupation() value = len(folios) res = { 'value': value, 'description': '', 'selector': {}, 'default_option': None, 'meta': 'prueba 1', 'in_thousands': None } return res @classmethod def report_number_guests(cls, args): # Dash Report folios = cls._get_occupation() value = 0 for folio in folios: value += len(folio['guests']) res = { 'value': value, 'description': '', 'selector': {}, 'default_option': None, 'meta': 'prueba 1', 'in_thousands': None } return res class FolioGuest(ModelSQL, ModelView): 'Folio Guest' __name__ = 'hotel.folio.guest' folio = fields.Many2One('hotel.folio', 'Folio', required=True, ondelete='CASCADE') main_guest = fields.Boolean('Main Guest') type_guest = fields.Selection([ ('adult', 'Adult'), ('child', 'Child'), ], 'Type Guest', required=True) type_guest_string = type_guest.translated('type_guest') nationality = fields.Many2One('country.country', 'Nationality') origin_country = fields.Many2One('country.country', 'Origin Country', select=True) target_country = fields.Many2One('country.country', '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', required=True) type_document_string = type_document.translated('type_document') id_number = fields.Char('Id Number', select=True, required=True) name = fields.Char('Name', select=True, required=True) mobile = fields.Char('Mobile', select=True, states={ 'required': Eval('main_guest', False) }) email = fields.Char('Email', select=True, states={ 'required': Eval('main_guest', False) }) profession = fields.Char('Profession', select=True) birthday = fields.Date('Birthday', select=True) visa_date = fields.Date('Visa Date', select=True) visa_category = fields.Char('Visa Category') visa_number = fields.Char('Visa Number') sex = fields.Selection([ ('female', 'Female'), ('male', 'Male'), ('', ''), ], 'Sex', required=True) sex_string = sex.translated('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') notes = fields.Text('Notes') @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, ('id_number', operator, value), ('name', operator, value), ('mobile', operator, value), ] return domain @fields.depends('name', 'first_name', 'second_name', 'first_family_name', 'second_family_name') def on_change_name(self): second_family_name = None first_family_name = None second_name = None first_name = None if self.name: 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 @staticmethod def default_type_guest(): return 'adult' @staticmethod def default_sex(): return 'male' @staticmethod def default_type_document(): return '13' @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 @fields.depends('id_number','name', 'sex', 'email', 'mobile', 'visa_number', 'visa_date', 'birthday', 'nationality') def on_change_id_number(self): if self.id_number: Guest = Pool().get('hotel.folio.guest') Party = Pool().get('party.party') guest = None guests = Guest.search([ ('id_number', '=', self.id_number) ]) if guests: guest = guests[0] else: parties = Party.search([ ('id_number', '=', self.id_number) ]) if parties: guest = parties[0] if guest: self.name = guest.name self.sex = guest.sex self.type_document = guest.type_document self.email = guest.email self.mobile = guest.mobile self.visa_number = guest.visa_number self.visa_date = guest.visa_date self.birthday = guest.birthday self.nationality = guest.nationality and guest.nationality.id self.profession = guest.profession self.first_name = guest.first_name self.second_name = guest.second_name self.first_family_name = guest.first_family_name self.second_family_name = guest.second_family_name # self.origin_country = guest.origin_country # self.target_country = guest.target_country @classmethod def manage_party(cls, v): Party = Pool().get('party.party') is_main_guest = v.pop('main_guest') folio = v.pop('folio', None) for val in ('type_guest', 'origin_country', 'target_country', 'profession'): _ = v.pop(val, None) if is_main_guest: parties = Party.search([ ('id_number', '=', v['id_number']), ]) email = v.pop('email', '') mobile = v.pop('mobile', '') v['type_person'] = 'persona_natural' if not parties: v['contact_mechanisms'] = [ ('create', [ {'type': 'email', 'value': email}, {'type': 'mobile', 'value': mobile}, ]) ] party, = Party.create([v]) else: Party.write(parties, v) party = parties[0] has_email = False has_mobile = False for cm in party.contact_mechanisms: if email and cm.type == 'email': cm.value = email has_email = True elif mobile and cm.type == 'mobile': cm.value = mobile has_mobile = True cm.save() to_write = [] if not has_mobile and mobile: to_write.append({'type': 'mobile', 'value': mobile}) if not has_email and email: to_write.append({'type': 'email', 'value': email}) if to_write: Party.write( parties, {'contact_mechanisms': [('create', to_write)]} ) if folio: Folio = Pool().get('hotel.folio') folio = Folio(folio) folio.main_guest = party.id folio.save() booking = folio.booking if not booking.party: booking.party = party.id booking.save() @classmethod def create(cls, vlist): super(FolioGuest, cls).create(vlist) for v in vlist: cls.manage_party(v) @classmethod def write(cls, records, values): super(FolioGuest, cls).write(records, values) fields = cls.fields_get() for val in ('id', 'rec_name', 'write_date', 'write_uid', 'create_date'): fields.pop(val) data = {} fields_list = fields.keys() rec = records[0] for field in fields_list: data[field] = getattr(rec, field) data.update(values) cls.manage_party(data) 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') 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, depends=['product']) 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']) storage = fields.Many2One('stock.location', 'Storage', domain=[('type', '=', 'storage')], states={ 'readonly': Bool(Eval('invoice_line')), }) move = fields.Many2One('stock.move', 'Move') @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 do_stock_move(self, name=None): pool = Pool() Location = pool.get('stock.location') Move = pool.get('stock.move') Config = Pool().get('hotel.configuration') # FIXME add origin locs_customer = Location.search([ ('type', '=', 'customer') ]) customer_loc_id = locs_customer[0].id config = Config.get_configuration() if not self.storage and config.storage_by_default: storage_id = config.storage_by_default.id else: storage_id = self.storage.id move, = Move.create([{ 'product': self.product.id, 'effective_date': self.date_service, 'quantity': self.quantity, 'unit_price': self.product.cost_price, 'uom': self.product.default_uom.id, 'from_location': storage_id, 'to_location': customer_loc_id, 'origin': str(self), }]) self.move = move.id Move.do([move]) self.save() def get_taxed_amount(self, name=None): if self.quantity and self.unit_price: return self.quantity * self.unit_price @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', 'folio', 'product', 'description', '_parent_folio.main_guest') def on_change_product(self): if self.product: self.unit_price = self.product.template.list_price self.description = self.product.template.name 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), ('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 and 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.name, 'party': op.booking.party.name if op.booking.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']) 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 class HotelFolioTax(ModelSQL): 'Hotel Folio - Tax' __name__ = 'hotel.folio-account.tax' _table = 'hotel_folio_account_tax' folio = fields.Many2One('hotel.folio', 'Folio', ondelete='CASCADE', select=True, required=True) tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT', select=True, required=True)