# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from __future__ import with_statement from datetime import datetime, timedelta from decimal import Decimal from trytond.model import Workflow, ModelView, ModelSQL, fields from trytond.wizard import Wizard, StateView, Button, StateTransition, StateReport from trytond.report import Report from trytond.pyson import Eval, If, In, Get, Not, Or, Equal, Bool from trytond.transaction import Transaction from trytond.pool import Pool __all__ = [ 'Booking', 'BookingLine', 'BookingReport', 'SelectRooms', 'SelectRoomsAsk', 'BookingVoucher', 'RoomsOccupancy', 'RoomsOccupancyStart', 'RoomsOccupancyReport', 'GuestsListReport', 'GuestsListStart', 'RegistrationCardReport', 'BookingForecastReport', 'GuestsList', 'Guest', 'BookingDailyStart', 'BookingDaily', 'BookingDailyReport' ] STATE = [ (None, ''), ('offer', 'Offer'), ('confirmed', 'Confirmed'), ('not_confirmed', 'Not Confirmed'), ('cancelled', 'Cancelled'), ] REGISTRATION_STATE = [ (None, ''), ('check_in', 'Check In'), ('check_out', 'Check Out'), ('no_show', 'No Show'), ] STATES = { 'readonly': Eval('state') != 'offer', } STATES_CONFIRMED = { 'readonly': Eval('state') != 'offer', 'required': Eval('state') == 'confirmed', } STATES_PROCESSING = { 'readonly': Eval('state').in_(['check_in', 'no_show', 'cancelled']), 'required': Eval('state') == 'check_in', } SEGMENT = [ ('', ''), ('sun_beach', 'Sun & Beach'), ('sport', 'Sport'), ('adventure', 'Adventure'), ('nature', 'Nature'), ('cultural', 'Cultural'), ('urban', 'Urban'), ('rural', 'Rural'), ('cruises', 'Cruises'), ('health', 'Health'), ('thematic_parks', 'Thematic Parks'), ('convention', 'Conventions & Meetings'), ('wedding', 'Weddings'), ('bussiness', 'Bussiness'), ] GUARANTEE = [ ('', ''), ('payment', 'Payment'), ('voucher', 'Voucher'), ('credit_card', 'Credit Card'), ('letter', 'Letter'), ] SATISFACTION = [ ('', ''), ('5', 'Excellent'), ('4', 'Good'), ('3', 'Regular'), ('2', 'Bad'), ('1', 'Worse'), ] MEDIA = [ ('', ''), ('phone', 'Phone'), ('fax', 'Fax'), ('mail', 'Mail'), ('chat', 'Chat'), ('direct', 'Direct'), ('web', 'Web'), ('channel_manager', 'Channel Manager'), ('other', 'Other'), ('ota', 'OTA'), ] PLAN = [ ('', ''), ('all_inclusive', 'All Inclusive'), ('bed_breakfast', 'Bed & Breakfast'), ('full_american', 'Full American'), ('half_american', 'Half American'), ] SOURCE = [ ('', ''), ('web', 'Web'), ('yellow_pages', 'Yellow Pages'), ('recommended', 'Recommended'), ('travel_agency', 'Travel Agency'), ('advertising', 'Advertising'), ('other', 'Other'), ] INVOICE_METHOD = [ ('by_booking', 'By Booking'), ('by_main_guest', 'By Main Guest'), # ('guest_customer', 'Guest / Customer') ] COMPLEMENTARY = [ ('', ''), ('in_house', 'In House'), ('courtesy', 'Courtesy') ] _ZERO = Decimal('0.0') class Booking(Workflow, ModelSQL, ModelView): 'Booking' __name__ = 'hotel.booking' _rec_name = 'number' number = fields.Char('Number', readonly=True, select=True, help="Sequence of reservation.") party = fields.Many2One('party.party', 'Party', required=False, select=True, help="Person or company owner of the booking.", states={ 'required': Eval('state') == 'check_in', 'readonly': Not(In(Eval('state'), ['offer', 'confirmed'])), }) contact = fields.Char('Contact', states=STATES_PROCESSING) payment_term = fields.Many2One('account.invoice.payment_term', 'Payment Term', states=STATES_PROCESSING) booking_date = fields.DateTime('Booking Date', readonly=False, states=STATES) person_num = fields.Integer('Person Number', states=STATES) group = fields.Boolean('Group', states=STATES) complementary = fields.Boolean('Complementary', states=STATES) type_complementary = fields.Selection(COMPLEMENTARY, 'Type Complementary', states={ 'invisible': ~Bool(Eval('complementary')), 'required': Bool(Eval('complementary')), }) party_seller = fields.Many2One('hotel.channel', 'Channel', states=STATES_PROCESSING, help="Agency or channel that do reservation.") state = fields.Selection(STATE, 'State', readonly=True, required=True) registration_state = fields.Selection(REGISTRATION_STATE, 'State Registration', readonly=True) state_string = state.translated('state') price_list = fields.Many2One('product.price_list', 'Price List', states={ 'readonly': Or(Not(Equal(Eval('state'), 'offer')), Bool(Eval('lines'))), }, depends=['state', 'company', 'lines']) company = fields.Many2One('company.company', 'Company', required=True, states=STATES, domain=[('id', If(In('company', Eval('context', {})), '=', '!='), Get(Eval('context', {}), 'company', 0))], readonly=True) lines = fields.One2Many('hotel.booking.line', 'booking', 'Lines', states={ 'required': Eval('state') == 'confirmed', 'readonly': Not(In(Eval('state'), ['offer', 'confirmed'])), }, depends=['state', 'party'], context={'party': Eval('party')}) cancellation_policy = fields.Many2One('hotel.policy.cancellation', 'Cancellation Policy') currency = fields.Many2One('currency.currency', 'Currency', required=True, states={ 'readonly': (Eval('state') != 'offer') | (Eval('lines', [0]) & Eval('currency', 0)), }, depends=['state']) invoice_method = fields.Selection(INVOICE_METHOD, 'Invoice Method', required=False, states=STATES_CONFIRMED) satisfaction = fields.Selection(SATISFACTION, 'Satisfaction') media = fields.Selection(MEDIA, 'Media', states=STATES_PROCESSING, help="Media from booking coming from.") media_string = media.translated('media') plan = fields.Selection(PLAN, 'Commercial Plan', states=STATES_PROCESSING, help="Plans offered by hotel and selected by guest for booking.") plan_string = plan.translated('plan') source_contact = fields.Selection(SOURCE, 'Source Contact', states=STATES_PROCESSING, help="Advertising source that create booking opportunity by guest.") source_contact_string = source_contact.translated('source_contact') comments = fields.Text('Comments', states=STATES_PROCESSING) segment = fields.Selection(SEGMENT, 'Tourism Segment', states=STATES_PROCESSING) segment_string = segment.translated('segment') guarantee = fields.Selection(GUARANTEE, 'Guarantee', states=STATES_PROCESSING) guarantee_string = guarantee.translated('guarantee') untaxed_amount = fields.Function(fields.Numeric('Untaxed Amount', digits=(16, 2), depends=['lines']), 'get_untaxed_amount') tax_amount = fields.Function(fields.Numeric('Tax Amount', digits=(16, 2), depends=['lines']), 'get_tax_amount') total_amount = fields.Function(fields.Numeric('Total Amount', digits=(16, 2), depends=['lines']), 'get_total_amount') operations = fields.Function(fields.One2Many('hotel.operation', None, 'Operations'), 'get_operations', searcher='search_operations') code = fields.Char('Code', states={'readonly': True}) booker = fields.Many2One('party.party', 'Booker', states={'readonly': True}) created_channel = fields.DateTime('Created Channel', states={'readonly': True}) # sales = fields.Many2Many('hotel.booking-sale.sale', 'booking', # 'sale', 'Sales', readonly=True) # operations = fields.Many2Many('hotel.booking-hotel.operation', 'booking', # 'sale', 'Sales', readonly=True) vouchers = fields.Many2Many('hotel.booking-account.voucher', 'booking', 'voucher', 'Vouchers', states=STATES_PROCESSING, domain=[ ], depends=['party']) vip = fields.Boolean('V.I.P. Customer', states=STATES) vehicles_num = fields.Integer('Vehicles Number', states=STATES, help="Number of vehicles that bring with guests.") vehicle_plate = fields.Integer('Vehicle Plate', states=STATES) travel_cause = fields.Char('Travel Cause', states=STATES) registration_card = fields.Char('Registration Card', readonly=True, select=True, help="Unique sequence of card registration.") taxes_exception = fields.Boolean('Taxes Exception', states=STATES) total_advance = fields.Function(fields.Numeric( 'Total Advance'), 'get_total_advance') @classmethod def __setup__(cls): super(Booking, cls).__setup__() cls._order.insert(0, ('create_date', 'DESC')) cls._transitions |= set(( ('offer', 'confirmed'), ('confirmed', 'offer'), ('offer', 'not_confirmed'), ('offer', 'cancelled'), ('cancelled', 'offer'), ('confirmed', 'cancelled'), ('not_confirmed', 'confirmed'), ('not_confirmed', 'cancelled'), )) cls._buttons.update({ 'select_rooms': { 'invisible': Eval('state') != 'offer', }, 'create_guest': { }, 'cancel': { 'invisible': Eval('state').in_(['cancel', '']) and Eval('registration_state') == 'check_out', }, 'offer': { 'invisible': ~Eval('state').in_([ 'confirmed', 'cancelled', 'not_confirmed' ]) and Eval('registration_state').in_(['check_in', 'check_out']), }, 'confirm': { 'invisible': ~Eval('state').in_(['offer', 'not_confirmed']) }, 'not_confirm': { 'invisible': Eval('state') != 'offer', }, 'check_in': { 'invisible': Eval('state') != 'confirmed' and Eval('registration_state').in_(['check_in', 'check_out']), }, 'no_show': { 'invisible': Eval('state') != 'confirmed' and Eval('registration_state').in_(['check_in', 'check_out']), }, 'check_out': { 'invisible': Eval('state') != 'confirmed' and Eval('registration_state') == 'check_out', }, 'pay_advance': { 'invisible': Eval('state').in_(['cancelled', 'offer', 'no_show']) and Eval('registration_state') == 'check_out', }, 'send_email': { 'invisible': Eval('state') != 'confirmed' }, }) cls._error_messages.update({ 'invalid_number_principal_guest': ('Must exist one principal guest'), 'payterm_missing': ('The payment term is missing!'), 'check_time_not_configured': ('The check out time is not configured!'), 'invalid_arrival_date': ('You cannot check in before the reserved date!'), 'missing_main_guest': ('Missing main guest in lines!'), 'missing_select_room': ('Missing select room!'), 'missing_default_configuration': ('Missing default configuration sequence!'), 'payterm_missing': ('The payment term is missing!'), 'room_no_clean': ('The room %s is not clean!'), 'missing_sequence_registration': ('Missing the configuration of \ registration card sequence!'), }) @classmethod def trigger_create(cls, records): cls.set_number(records) @classmethod def copy(cls, bookings, default=None): if default is None: default = {} default = default.copy() default['number'] = None default['registration_card'] = None default['state'] = 'offer' default['booking_date'] = datetime.now() return super(Booking, cls).copy(bookings, default=default) @staticmethod def default_currency(): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: return Company(company).currency.id @staticmethod def default_company(): return Transaction().context.get('company') or False @staticmethod def default_payment_term(): PaymentTerm = Pool().get('account.invoice.payment_term') payment_term = PaymentTerm.search([]) if len(payment_term) >= 1: return payment_term[0].id @staticmethod def default_state(): return 'offer' @staticmethod def default_invoice_method(): return 'by_booking' @staticmethod def default_booking_date(): now = datetime.now() return now def get_operations(self, name): Operation = Pool().get('hotel.operation') operations = [] for line in self.lines: ops = Operation.search([ ('origin', '=', 'hotel.booking.line,' + str(line.id)) ]) if ops: ids_op = [o.id for o in ops] operations.extend(ids_op) return operations def get_person_num(self, name): res = 0 for line in self.lines: if line.num_adult: res += line.num_adult if line.num_children: res += line.num_children return res @fields.depends('party', 'price_list') def on_change_party(self): if self.party and self.party.sale_price_list: self.price_list = self.party.sale_price_list.id self.price_list.rec_name = self.party.sale_price_list.rec_name @fields.depends('party_seller') def on_change_party_seller(self): if self.party_seller and self.party_seller.price_list: self.price_list = self.party_seller.price_list @classmethod @ModelView.button_action('hotel.wizard_select_rooms') def select_rooms(cls, bookings): pass @classmethod @ModelView.button_action('hotel.wizard_party_guest') def create_guest(cls, bookings): pass @classmethod @ModelView.button_action('hotel.wizard_booking_advance_voucher') def pay_advance(cls, bookings): pass @classmethod @ModelView.button @Workflow.transition('offer') def offer(cls, records): for record in records: record.delete_occupancy() @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, records): for record in records: record.cancel_occupancy() @classmethod @ModelView.button @Workflow.transition('not_confirmed') def not_confirm(cls, records): pass @classmethod @ModelView.button @Workflow.transition('confirmed') def confirm(cls, records): # Occupancy must be created only when booking is confirmed cls.set_number(records) for rec in records: # FIXME check if does not exist previous occupancy if exist update state rec.create_occupancy() # @classmethod # @ModelView.button # def check_in(cls, records): # CHECK_INDIVIDUAL = False # Line = Pool().get('hotel.booking.line') # check_in_(records,CHECK_INDIVIDUAL) # for record in records: # for line in record.lines: # Line.write([line], {'registration_state_': 'check_in'}) # # config_party = Pool().get('party.configuration')(1) # # validate_party = config_party.validate_party # # if not validate_party: # # config_party.validate_party = True # # config_party.save() # # for record in records: # # record.party.pre_validate() # # cls.set_registration_card_number([record]) # # for line in record.lines: # # if line.main_guest is None: # # cls.raise_user_error('missing_main_guest') # # if line.room is None: # # cls.raise_user_error('missing_select_room') # # record.check_rooms() # # cls.write([record], {'registration_state': 'check_in'}) # # record.update_occupancy(state='open') # # if config_party.validate_party != validate_party: # # config_party.validate_party = validate_party # # config_party.save() @classmethod @ModelView.button def no_show(cls, records): check = False for record in records: record.update_occupancy(state='cancelled',records=records,check=check) cls.write([record], {'registration_state': 'no_show'}) record.cancel_occupancy() # @classmethod # @ModelView.button # def check_out(cls, records): # CHECK_INDIVIDUAL = False # Line = Pool().get('hotel.booking.line') # for record in records: # cls.write([record], {'registration_state': 'check_out'}) # update_occupancy(state='closed',check=CHECK_INDIVIDUAL,records=records) # for line in record.lines: # Line.write([line], {'registration_state_': 'check_out'}) @classmethod @ModelView.button def send_email(cls, records): for reserve in records: if reserve.state == 'confirmed': reserve.send_email_to() @classmethod def set_number(cls, bookings): """ Fill the number field with the booking sequence """ pool = Pool() Sequence = pool.get('ir.sequence') Config = pool.get('hotel.configuration') config = Config.get_configuration() for booking in bookings: if booking.number or not config.booking_sequence: continue number = Sequence.get_id(config.booking_sequence.id) cls.write([booking], {'number': number}) def cancel_occupancy(self): Operation = Pool().get('hotel.operation') ops = Operation.search([('origin', '=', str(self))]) if ops: Operation.cancel(ops) @classmethod def set_registration_card_number(cls, bookings): """ Fill the number field for registration card with sequence """ pool = Pool() Sequence = pool.get('ir.sequence') Config = pool.get('hotel.configuration') config = Config.get_configuration() for booking in bookings: if booking.registration_card: continue if not config.registration_card_sequence: cls.raise_user_error('missing_sequence_registration') number = Sequence.get_id(config.registration_card_sequence.id) cls.write([booking], {'registration_card': number}) def delete_occupancy(self): Operation = Pool().get('hotel.operation') for l in self.lines: if l.operation: Operation.delete([l.operation]) def create_occupancy(self): pool = Pool() Config = pool.get('hotel.configuration') Line = pool.get('hotel.booking.line') config = Config.get_configuration() if config.check_out_time is None or config.check_in_time is None: self.raise_user_error('check_time_not_configured') if self.invoice_method == 'by_booking': to_create = [(self.party, l, l.product.id, l.unit_price) for l in self.lines] elif self.invoice_method == 'guest_customer': to_create = [ (self.party, l, l.product.id, l.unit_price) for l in self.lines ] to_create.extend([(l.main_guest, l, None, None) for l in self.lines]) else: to_create = [(l.main_guest, l, l.product.id, l.unit_price) for l in self.lines] for value in to_create: Line.create_operation(value[0], value[1], value[2], value[3]) def check_rooms(self): pool = Pool() rooms_ids = [] for line in self.lines: rooms_ids.append(line.room.id) Housekeeping = pool.get('hotel.housekeeping') housekeepings = Housekeeping.search([ ('state', '!=', 'clean') ]) for hk in housekeepings: if hk.room.id in rooms_ids: self.raise_user_error('room_no_clean', hk.room.name) def get_context_price(self, product): context = {} if getattr(self, 'currency', None): context['currency'] = self.currency.id if getattr(self, 'party', None): context['customer'] = self.party.id if getattr(self, 'booking_date', None): context['sale_date'] = self.booking_date if getattr(self, 'price_list', None): context['price_list'] = self.price_list.id context['uom'] = product.template.default_uom.id # Set taxes before unit_price to have taxes in context of sale price taxes = [] pattern = {} if not self.taxes_exception: for tax in product.customer_taxes_used: if self.party and self.party.customer_tax_rule: tax_ids = self.party.customer_tax_rule.apply(tax, pattern) if tax_ids: taxes.extend(tax_ids) continue taxes.append(tax.id) if self.party and self.party.customer_tax_rule: tax_ids = self.party.customer_tax_rule.apply(None, pattern) if tax_ids: taxes.extend(tax_ids) context['taxes'] = taxes return context def update_occupancy(self, state, records, check): Operation = Pool().get('hotel.operation') OperationVoucher = Pool().get('hotel.operation-account.voucher') vouchers_added = False lines = records if check == True else self.lines for line in lines: # if state != 'open': # continue guests = [{ 'party': line.main_guest.id }] for g in line.guests: # if line.main_guest.id == g.party.id: # continue guests.append({ 'party': g.party.id }) if line.operation: Operation.write([line.operation], { 'state': state, 'main_guest': line.main_guest.id, 'party': line.booking.party.id, 'unit_price': line.unit_price, 'start_date': line.arrival_date, 'end_date': line.departure_date, 'reference': line.booking.registration_card, 'guests': [('create', guests)] }) vouchers = None if not vouchers_added and self.vouchers: vouchers = self.vouchers vouchers_added = True if vouchers: for v in vouchers: OperationVoucher.create([{ 'voucher': v.id, 'operation': line.operation.id }]) def get_total_advance(self, name): Advance = Pool().get('hotel.booking-account.voucher') res = 0 vouchers = Advance.search([ ('booking', '=', self.id), ]) if vouchers: for voucher in vouchers: res += voucher.voucher.amount_to_pay return res def get_total_amount(self, name): res = 0 if self.tax_amount or self.untaxed_amount: res = self.tax_amount + self.untaxed_amount return res def get_taxes(self, product): ctx = self.get_context_price(product) return ctx['taxes'] def get_tax_amount(self, name): Tax = Pool().get('account.tax') res = _ZERO for line in self.lines: taxes_ids = self.get_taxes(line.product) if taxes_ids: taxes = Tax.browse(taxes_ids) tax_list = Tax.compute( taxes, line.unit_price or _ZERO, line.nights_quantity or 0 ) tax_amount = sum([t['amount'] for t in tax_list], _ZERO) res += tax_amount res = Decimal(round(res, 2)) return res def get_untaxed_amount(self, name): res = _ZERO for line in self.lines: res += line.total_amount return res def get_occupancies(self, name): occupancies = set() for line in self.lines: if line.occupancy_line: occupancies.add(line.occupancy_line.id) return list(occupancies) def send_email_to(self): pool = Pool() config = pool.get('hotel.configuration')(1) Template = pool.get('email.template') email = self.party.email if email: Template.send(config.booking_email_template, self, email) else: self.get_message('El usuario no tiene un correo asociado.') def get_message(self, message): self.raise_user_error(message) class BookingLine(ModelSQL, ModelView): 'Booking Line' __name__ = 'hotel.booking.line' booking = fields.Many2One('hotel.booking', 'Booking', ondelete='CASCADE', select=True) room = fields.Many2One('hotel.room', 'Room', select=True, states={ 'required': Eval('state') == 'confirmed', }) arrival_date = fields.Date('Arrival Date', required=True) departure_date = fields.Date('Departure Date', required=True) product = fields.Many2One('product.product', 'Product', select=True, domain=[ ('template.type', '=', 'service'), ('template.kind', '=', 'accommodation'), ], required=True) unit_price = fields.Numeric('Unit Price', digits=(16, 4), states={ 'required': Bool(Eval('product')), }) uom = fields.Many2One('product.uom', 'UOM', readonly=True) main_guest = fields.Many2One('party.party', 'Main Guest', select=True, states={ 'required': Eval('state').in_(['check_in', 'check_out']) } ) state = fields.Selection(STATE, 'State') contact = fields.Char('Contact') num_children = fields.Function(fields.Integer('No. Children'), 'get_num_children') nights_quantity = fields.Function(fields.Integer('Nights'), 'get_nights_quantity') host_quantity = fields.Integer('Host') 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_total_amount') total_commission = fields.Function(fields.Numeric('Channel Commission', digits=(16, 2)), 'get_channel_commission') guests = fields.One2Many('hotel.booking.guest', 'booking_line', 'Guests') operation = fields.Many2One('hotel.operation', 'Operation') 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) registration_state = fields.Selection(REGISTRATION_STATE, 'State Registration', readonly=True) @classmethod def __setup__(cls): super(BookingLine, cls).__setup__() cls._error_messages.update({ 'invalid_number_guests': ('Invalid number of guests for ' 'the room %s.'), 'accommodation_not_defined': ('The room %s has not accommodation'), 'invalid_date': ('Departure date must be greater \ that arrival date'), 'occupied_room': ('The room is occupied in the date %s'), 'restring_room': ('The room %s is in restring access for quarantine protocolo!'), }) cls._buttons.update({ 'check_in': { 'invisible': Eval('booking.state') != 'confirmed' and Eval('registration_state').in_(['check_in', 'check_out']), }, 'check_out': { 'invisible': Eval('booking.state') != 'confirmed' and Eval('registration_state') == 'check_out', }}) @classmethod def write(cls, *args): Operation = Pool().get('hotel.operation') lines, vals = args[0], args[-1] for line in lines: if not line.operation: continue values = {} if vals.get('room'): values.update({ 'room': vals['room'], }) if vals.get('main_guest'): values.update({ 'party': vals['main_guest'], }) if vals.get('product'): values.update({ 'accommodation': vals['product'], }) if vals.get('state'): values.update({'state': vals['state']}) if vals.get('arrival_date'): values.update({'start_date': vals['arrival_date']}) if vals.get('departure_date'): values.update({'end_date': vals['departure_date']}) if values: Operation.write([line.operation], values) super(BookingLine, cls).write(*args) @classmethod @ModelView.button def check_in(cls, records): check = True pool = Pool() Booking = pool.get('hotel.booking') config_party = Pool().get('party.configuration')(1) validate_party = config_party.validate_party if not validate_party: config_party.validate_party = True config_party.save() config_party.save() record = records[0].booking record.party.pre_validate() Booking.set_registration_card_number([record]) line = records[0] if line.main_guest is None: Booking.raise_user_error('missing_main_guest') if line.room is None: Booking.raise_user_error('missing_select_room') record.check_rooms() cls.write([records[0]], {'registration_state': 'check_in'}) record.update_occupancy(state='open',records=records,check=check) if config_party.validate_party != validate_party: config_party.validate_party = validate_party config_party.save() @classmethod @ModelView.button def check_out(cls, records): check = True for record in records: cls.write([record], {'registration_state': 'check_out'}) record.booking.update_occupancy(state='closed',records=records,check=check) pass @staticmethod def default_main_guest(): party = Transaction().context.get('party') return party @staticmethod def default_host_quantity(): return 1 @staticmethod def default_state(): return 'offer' @staticmethod def default_accommodation(): Configuration = Pool().get('hotel.configuration') configuration = Configuration.get_configuration() if configuration.default_accommodation: return configuration.default_accommodation.id @classmethod def validate(cls, lines): super(BookingLine, cls).validate(lines) for line in lines: line.check_method() pass @classmethod def create_operation(cls, party, line, accommodation, unit_price): Operation = Pool().get('hotel.operation') kind = 'occupancy' # if not accommodation: # kind = 'statement' values = { 'kind': kind, 'accommodation': accommodation, 'start_date': line.arrival_date, 'end_date': line.departure_date, 'currency': line.booking.currency.id, 'state': 'draft', 'unit_price': unit_price, 'party': party.id if party else None, 'origin': str(line), 'complementary': line.booking.complementary, 'type_complementary': line.booking.type_complementary, 'taxes_exception': line.booking.taxes_exception, } if line.room: values['room'] = line.room.id if line.main_guest: values['main_guest'] = line.main_guest.id operation, = Operation.create([values]) line.write([line], {'operation': operation.id}) @fields.depends('accommodation', 'product') def on_change_accommodation(self): if not self.accommodation: self.product = None @fields.depends('product', 'unit_price', 'uom', 'booking', 'nights_quantity') 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 @fields.depends('arrival_date', 'departure_date') def on_change_arrival_date(self): if not self.arrival_date or (self.departure_date and self.departure_date > self.arrival_date): return self.departure_date = self.arrival_date + timedelta(days=1) def check_method(self): """ Check the methods. """ Date = Pool().get('ir.date') if self.arrival_date < Date.today(): self.raise_user_error('invalid_arrival_date') if self.arrival_date >= self.departure_date: self.raise_user_error('invalid_date') Operation = Pool().get('hotel.operation') operations = Operation.search([ ('room', '=', self.room.id), ['AND', ['OR', [ ('start_date', '>=', self.arrival_date), ('end_date', '<=', self.arrival_date), ], [ ('start_date', '>=', self.departure_date), ('end_date', '<=', self.departure_date), ], ]] ]) print(operations) if operations: self.raise_user_error('occupied_room', self.departure_date) config = Pool().get('hotel.configuration')(1) quarantine_days = config.quarantine_rooms room_id = self.room.id if quarantine_days: delta = timedelta(days=int(quarantine_days)) _date = self.arrival_date - delta operations = Operation.search([ ['AND', ['OR', [ ('room', '=', room_id), ('end_date', '=', _date) ], [ ('room', '=', room_id), ('end_date', '=', self.arrival_date) ]]]]) print(operations) if operations: self.raise_user_error('restring_room', self.room.name) 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 @fields.depends('quantity', 'arrival_date', 'departure_date') def get_nights_quantity(self, name=None): """ Compute nights between start and end return a integer the mean days of occupancy. """ nights = 0 if not self.arrival_date or not self.departure_date: return nights nights = (self.departure_date - self.arrival_date).days return nights def get_total_amount(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 = _ZERO if self.nights_quantity and self.unit_price: res = self.nights_quantity * self.unit_price return res def get_channel_commission(self, name): """ Calculation of sale commission for channel based on booking total amount """ res = Decimal(0) if self.total_amount and self.booking.party_seller and \ self.booking.party_seller.party.sale_commission: res = self.total_amount * self.booking.party_seller.sale_commission / 100 return res class BookingReport(Report): __name__ = 'hotel.booking' @classmethod def get_context(cls, records, data): report_context = super(BookingReport, cls).get_context(records, data) user = Pool().get('res.user')(Transaction().user) report_context['company'] = user.company return report_context class SelectRoomsAsk(ModelView): 'Filter Rooms Assistant' __name__ = 'hotel.booking.select_rooms.ask' arrival_date = fields.Date('Arrival Date', required=True) departure_date = fields.Date('Departure Date', required=True) accommodation = fields.Many2One('product.product', 'Accommodation', domain=[ ('template.kind', '=', 'accommodation'), ]) rooms = fields.Many2Many('hotel.room', None, None, 'Rooms', domain=[ ('id', 'in', Eval('targets')), ]) overbooking = fields.Boolean('Overbooking') targets = fields.Function(fields.Many2Many('hotel.room', None, None, 'Targets'), 'on_change_with_targets') @staticmethod def default_accommodation(): Configuration = Pool().get('hotel.configuration') configuration = Configuration.get_configuration() if configuration.default_accommodation: return configuration.default_accommodation.id @fields.depends('arrival_date', 'departure_date', 'accommodation', 'overbooking') def on_change_with_targets(self, name=None): pool = Pool() Operation = pool.get('hotel.operation') RoomTemplate = pool.get('hotel.room-product.template') Room = pool.get('hotel.room') res = [] if not self.accommodation or not self.arrival_date or not self.departure_date: return res if self.overbooking: return [r.id for r in Room.search([])] room_templates = RoomTemplate.search([ ('template.accommodation_capacity', '>=', self.accommodation.accommodation_capacity) ]) rooms_ids = [t.room.id for t in room_templates] rooms_available_ids = Operation.get_available_rooms( self.arrival_date, self.departure_date, rooms_ids=rooms_ids ) return rooms_available_ids class SelectRooms(Wizard): 'Select Rooms' __name__ = 'hotel.booking.select_rooms' """ this is the wizard that allows the front desk employee to select rooms, based on the requirements listed by the customer. """ start = StateView('hotel.booking.select_rooms.ask', 'hotel.view_select_rooms_form', [ Button('Exit', 'end', 'tryton-cancel'), Button('Add and Continue', 'add_continue', 'tryton-forward'), Button('Add', 'add_rooms', 'tryton-ok'), ] ) add_rooms = StateTransition() add_continue = StateTransition() def transition_add_rooms(self): self._add_rooms() return 'end' def transition_add_continue(self): self._add_rooms() return 'start' def _add_rooms(self): pool = Pool() Line = pool.get('hotel.booking.line') Booking = pool.get('hotel.booking') booking = Booking(Transaction().context.get('active_id')) lines_to_create = [] ctx = {} if booking.price_list: ctx['price_list'] = booking.price_list ctx['sale_date'] = self.start.arrival_date ctx['currency'] = booking.currency.id if booking.party: ctx['customer'] = booking.party.id product = self.start.accommodation quantity = (self.start.departure_date - self.start.arrival_date).days unit_price = product.template.list_price if booking.price_list: with Transaction().set_context(ctx): unit_price = booking.price_list.compute( booking.party, product, unit_price, quantity, product.default_uom) unit_price = booking.currency.round(unit_price) for room in self.start.rooms: values = { 'booking': booking.id, 'product': product.id, 'room': room.id, 'arrival_date': self.start.arrival_date, 'departure_date': self.start.departure_date, 'unit_price': unit_price, } if booking.party: values['main_guest'] = booking.party.id values.update({'product': product.id}) lines_to_create.append(values) Line.create(lines_to_create) booking.save() # class BookingSale(ModelSQL): # 'Booking - Sale' # __name__ = 'hotel.booking-sale.sale' # _table = 'booking_sales_rel' # booking = fields.Many2One('hotel.booking', 'Booking', ondelete='CASCADE', # required=True) # sale = fields.Many2One('sale.sale', 'Sale', ondelete='RESTRICT', # required=True) class BookingVoucher(ModelSQL): 'Booking - Voucher' __name__ = 'hotel.booking-account.voucher' _table = 'booking_vouchers_rel' booking = fields.Many2One('hotel.booking', 'Booking', ondelete='CASCADE', select=True, required=True) voucher = fields.Many2One('account.voucher', 'Voucher', select=True, domain=[('voucher_type', '=', 'receipt')], ondelete='RESTRICT', required=True) @classmethod def set_voucher_origin(cls, voucher_id, booking_id): cls.create([{ 'voucher': voucher_id, 'booking': booking_id, }]) class Guest(ModelSQL, ModelView): 'Guest' __name__ = 'hotel.booking.guest' _rec_name = 'party' booking_line = fields.Many2One('hotel.booking.line', 'Booking Line', 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(Guest, cls).create(new_values) class BookingForecastStart(ModelView): 'Booking Forecast Start' __name__ = 'hotel.print_booking_forecast.start' date = fields.Date('Start Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) @staticmethod def default_date(): Date_ = Pool().get('ir.date') return Date_.today() @staticmethod def default_company(): return Transaction().context.get('company') class BookingForecast(Wizard): 'Booking Forecast' __name__ = 'hotel.print_booking_forecast' start = StateView('hotel.print_booking_forecast.start', 'hotel.print_booking_forecast_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Print', 'print_', 'tryton-print', default=True), ]) print_ = StateReport('hotel.booking_forecast.report') def do_print_(self, action): company = self.start.company data = { 'date': self.start.date, 'company': company.id, } return action, data def transition_print_(self): return 'end' class BookingForecastReport(Report): __name__ = 'hotel.booking_forecast.report' @classmethod def get_context(cls, records, data): report_context = super(BookingForecastReport, cls).get_context(records, data) MAX_DAYS = 30 pool = Pool() Company = pool.get('company.company') Room = pool.get('hotel.room') BookingLine = pool.get('hotel.booking.line') rooms = Room.search([]) alldays = {} alldays_convert = {} for nd in range(MAX_DAYS): day_n = 'day' + str((nd + 1)) tdate = data['date'] + timedelta(nd) data[day_n] = tdate data['total_' + day_n] = 0 data[('revenue_' + day_n)] = 0 data[('rate_' + day_n)] = 0 alldays[day_n] = '' alldays_convert[tdate] = day_n date_init = data['date'] date_limit = data['date'] + timedelta(MAX_DAYS) dom = [['OR', [ ('arrival_date', '<=', date_init), ('departure_date', '>=', date_init), ], [ ('arrival_date', '>=', date_init), ('arrival_date', '<=', date_limit), ], ], ('booking.state', 'not in', ['no_show', 'cancelled'])] lines = BookingLine.search(dom) drooms = {} for room in rooms: drooms[room.id] = {'name': room.name} drooms[room.id].update(alldays.copy()) for line in lines: _delta = (line.departure_date - line.arrival_date).days for i in range(_delta): dt = line.arrival_date + timedelta(i) if dt >= date_init and dt < date_limit \ and dt >= data['date']: dayn = alldays_convert[dt] drooms[line.room.id][dayn] = "X" data['total_' + dayn] += 1 data['revenue_' + dayn] += float(line.unit_price) / 1000000 for i in range(MAX_DAYS): day_n = 'day' + str((i + 1)) data['rate_' + day_n] = (data['total_' + day_n] * 100.0) / len(rooms) report_context['records'] = list(drooms.values()) report_context['date'] = data['date'] report_context['company'] = Company(data['company']).party.name return report_context class RegistrationCardReport(Report): __name__ = 'hotel.occupancy.registration_card' @classmethod def get_context(cls, records, data): report_context = super(RegistrationCardReport, cls).get_context(records, data) user = Pool().get('res.user')(Transaction().user) report_context['company'] = user.company 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, data): report_context = super(GuestsListReport, cls).get_context(records, data) pool = Pool() Company = pool.get('company.company') Operation = pool.get('hotel.operation') Room = pool.get('hotel.room') User = pool.get('res.user') user_id = Transaction().user user = User(user_id) start_date = data['date'] operations = Operation.search([ ('start_date', '<=', start_date), ('kind', '=', 'occupancy'), ('state', '=', 'check_in'), ], order=[('room.code', 'ASC')]) total_guests = [] rooms_occupied = [] for op in operations: total_guests.append(len(op.guests)) rooms_occupied.append(op.room.id) rooms = Room.search_read([]) _rooms_occupied = len(set(rooms_occupied)) occupancy_rate = round(_rooms_occupied / len(rooms), 2) report_context['records'] = operations 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 RoomsOccupancyStart(ModelView): 'Rooms Occupancy Start' __name__ = 'hotel.print_rooms_occupancy.start' date = fields.Date('Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) @staticmethod def default_date(): Date_ = Pool().get('ir.date') return Date_.today() @staticmethod def default_company(): return Transaction().context.get('company') class RoomsOccupancy(Wizard): 'Rooms Occupancy' __name__ = 'hotel.print_rooms_occupancy' start = StateView('hotel.print_rooms_occupancy.start', 'hotel.print_rooms_occupancy_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Open', 'print_', 'tryton-print', default=True), ]) print_ = StateReport('hotel.rooms_occupancy.report') def do_print_(self, action): company = self.start.company data = { 'date': self.start.date, 'company': company.id, } return action, data def transition_print_(self): return 'end' class RoomsOccupancyReport(Report): __name__ = 'hotel.rooms_occupancy.report' @classmethod def get_context(cls, records, data): report_context = super(RoomsOccupancyReport, cls).get_context(records, data) pool = Pool() Company = pool.get('company.company') Room = pool.get('hotel.room') Operation = pool.get('hotel.operation') start_date = data['date'] all_rooms = Room.search([], order=[('code', 'ASC')]) operations = Operation.search([ ('start_date', '<=', start_date), ('state', '=', 'check_in'), ('kind', '=', 'occupancy'), ]) def _get_default_room(r): res = { 'room': r.name, 'guest': None, 'num_guest': None, 'party': None, 'arrival': None, 'departure': None, 'booking': None, 'reference': None, 'amount': None, 'balance': None, 'state': None, } return res rooms_map = {room.id: _get_default_room(room) for room in all_rooms} occupancy_rooms = 0 for op in operations: rooms_map[op.room.id].update({ 'guest': op.main_guest.name if op.main_guest else op.party.name, 'num_guest': len(op.guests), 'party': op.party.name, 'arrival': op.start_date, 'departure': op.end_date, 'reference': op.reference, 'amount': op.total_amount, 'balance': op.total_amount_today, 'state': op.state, }) occupancy_rooms += 1 if all_rooms: occupancy_rate = (float(len(operations)) / len(all_rooms)) * 100 else: occupancy_rate = 0 user = Pool().get('res.user')(Transaction().user) report_context['records'] = rooms_map report_context['occupancy_rate'] = occupancy_rate report_context['occupancy_rooms'] = occupancy_rooms report_context['company'] = Company(data['company']).party.name report_context['date'] = data['date'] report_context['user'] = user.rec_name return report_context class BookingDailyStart(ModelView): 'Booking Daily Start' __name__ = 'hotel.print_booking_daily.start' date = fields.Date('Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) @staticmethod def default_date(): Date_ = Pool().get('ir.date') return Date_.today() @staticmethod def default_company(): return Transaction().context.get('company') class BookingDaily(Wizard): 'Rooms Occupancy' __name__ = 'hotel.booking_daily' start = StateView('hotel.print_booking_daily.start', 'hotel.print_booking_daily_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Open', 'print_', 'tryton-print', default=True), ]) print_ = StateReport('hotel.booking_daily.report') def do_print_(self, action): company = self.start.company data = { 'date': self.start.date, 'company': company.id, } return action, data def transition_print_(self): return 'end' class BookingDailyReport(Report): __name__ = 'hotel.booking_daily.report' @classmethod def get_context(cls, records, data): report_context = super(BookingDailyReport, cls).get_context(records, data) pool = Pool() Company = pool.get('company.company') records = BookingLine.search([ ('arrival_date', '=', data['date']), ], order=[('room.code', 'ASC')]) report_context['records'] = records report_context['company'] = Company(data['company']).party.name report_context['date'] = data['date'] return report_context