# 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, date 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 from trytond.exceptions import UserError from trytond.i18n import gettext from .constants import ( STATE_BOOKING, REGISTRATION_STATE, REASON, GUARANTEE, SATISFACTION, MEDIA, PLAN, INVOICE_METHOD, COMPLEMENTARY, PAYMENT_METHOD_CHANNEL, ) STATES = { 'readonly': Eval('state') != 'offer', } STATES_CONFIRMED = { 'readonly': Eval('state') != 'offer', 'required': Eval('state') == 'confirmed', } STATES_CHECKIN = { 'readonly': Eval('registration_state').in_( ['check_in', 'no_show', 'cancelled'] ), 'required': Eval('registration_state') == 'check_in', } _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', 'Customer', 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_CHECKIN, help='Main contact or person how request booking') payment_term = fields.Many2One('account.invoice.payment_term', 'Payment Term', states=STATES_CHECKIN) 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')), }) # rename to channel channel = fields.Many2One('hotel.channel', 'Channel', states={ 'invisible': Eval('media') != 'ota', 'readonly': Eval('state').in_(['confirmed', 'cancelled']), }, help="Agency or channel that do reservation.") state = fields.Selection(STATE_BOOKING, 'State', readonly=True, required=True) registration_state = fields.Selection(REGISTRATION_STATE, 'State Registration', readonly=True, depends=['lines']) 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.folio', 'booking', 'Lines', states={ 'required': Eval('state') == 'confirmed', 'readonly': Eval('registration_state').in_(['check_in', 'check_out']), }, depends=['state', 'party'], context={'party': Eval('party')}) cancellation_policy = fields.Many2One('hotel.policy.cancellation', 'Cancellation Policy', states=STATES_CHECKIN) 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_CHECKIN, help="Media from booking coming from.") media_string = media.translated('media') plan = fields.Selection(PLAN, 'Commercial Plan', states=STATES_CHECKIN, help="Plans offered by hotel and selected by guest for booking.") plan_string = plan.translated('plan') comments = fields.Text('Comments', states=STATES_CHECKIN) reason = fields.Selection(REASON, 'Tourism Segment', states=STATES_CHECKIN) reason_string = reason.translated('segment') guarantee = fields.Selection(GUARANTEE, 'Guarantee', states=STATES_CHECKIN) 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') code = fields.Char('Code', states={'readonly': True}) booker = fields.Many2One('party.party', 'Booker', states={'readonly': True}) created_channel = fields.DateTime('Created Channel', states={'readonly': True}) vouchers = fields.Many2Many('hotel.booking-account.voucher', 'booking', 'voucher', 'Vouchers', states=STATES_CHECKIN, domain=[ ], depends=['party']) vip = fields.Boolean('V.I.P. Customer', states=STATES) ota_booking_code = fields.Char('OTA Code', select=True, states={'invisible': Eval('media') != 'ota'} ) 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) taxes_exception = fields.Boolean('Taxes Exception', states=STATES) total_advance = fields.Function(fields.Numeric('Total Advance', digits=(16, 2)), 'get_total_advance') pending_to_pay = fields.Function(fields.Numeric('Pending to Pay', digits=(16, 2)), 'get_pending_to_pay') breakfast_included = fields.Boolean('Breakfast Included') channel_commission = fields.Function(fields.Numeric('Channel Commission', digits=(16, 2), depends=['lines']), 'get_channel_commission') channel_invoice = fields.Many2One('account.invoice', 'Channel Invoice', states={ 'invisible': ~Eval('channel'), 'readonly': True }) channel_paymode = fields.Selection(PAYMENT_METHOD_CHANNEL, 'Channel Paymode', states={'invisible': ~Eval('channel')}, depends=['channel'] ) @classmethod def __setup__(cls): super(Booking, cls).__setup__() cls._order.insert(0, ('create_date', 'DESC')) cls._transitions |= set(( ('offer', 'confirmed'), ('offer', 'cancelled'), ('confirmed', 'offer'), ('cancelled', 'offer'), ('confirmed', 'cancelled'), ('confirmed', 'not_show'), )) cls._buttons.update({ 'select_rooms': { 'invisible': Eval('state').in_(['finished', 'cancelled', 'not_show']), }, 'create_guest': { 'invisible': Eval('party'), }, 'cancel': { 'invisible': Eval('state').in_( ['cancelled', 'not_show', 'finished']) }, 'offer': { 'invisible': Eval('state').in_(['offer', 'confirmed']) }, 'confirm': { 'invisible': ~Eval('state').in_(['offer', 'not_show']) }, 'not_show': { 'invisible': Eval('state') != 'confirmed', }, 'do_pay': { 'invisible': Eval('state').in_(['offer', 'cancelled']), }, 'send_email': { 'invisible': Eval('state') != 'confirmed' }, 'bill': { 'invisible': ~Eval('state').in_(['confirmed', 'not_show']), }, 'bill_to_channel': { 'invisible': ~Eval('channel'), 'readonly': (Eval('channel_paymode') != 'ota_collect') | Eval('channel_invoice', True), }, }) @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['state'] = 'offer' default['booking_date'] = datetime.now() return super(Booking, cls).copy(bookings, default=default) @classmethod def search_rec_name(cls, name, clause): _, operator, value = clause if operator.startswith('!') or operator.startswith('not '): bool_op = 'AND' else: bool_op = 'OR' domain = [ bool_op, ('number', operator, value), ('ota_booking_code', operator, value), ('party.name', operator, value), ('contact', operator, value), ] return domain @staticmethod def default_currency(): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: return Company(company).currency.id @staticmethod def default_company(): return Transaction().context.get('company') or False @staticmethod def default_state(): return 'offer' @staticmethod def default_plan(): return 'bed_breakfast' @staticmethod def default_invoice_method(): return 'by_booking' @staticmethod def default_booking_date(): now = datetime.now() return now 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('ota_booking_code', 'lines') def on_change_ota_booking_code(self): if self.ota_booking_code: for line in self.lines: line.reference = self.ota_booking_code @fields.depends('party', 'price_list', 'lines') def on_change_party(self): if self.party: if self.party.sale_price_list: self.price_list = self.party.sale_price_list.id self.price_list.rec_name = self.party.sale_price_list.rec_name for folio in self.lines: if self.party.type_document != '31': folio.main_guest = self.party.id @fields.depends('channel') def on_change_channel(self): if self.channel: self.channel_paymode = self.channel.payment_method if self.channel.price_list: self.price_list = self.channel.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 do_pay(cls, bookings): pass @classmethod @ModelView.button @Workflow.transition('offer') def offer(cls, records): pass @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, records): for rec in records: for folio in rec.lines: if folio.registration_state in ['check_in', 'check_out']: raise UserError(gettext('hotel.msg_no_delete_folios')) else: folio.delete([folio]) @classmethod @ModelView.button @Workflow.transition('not_show') def not_show(cls, records): pass @classmethod @ModelView.button @Workflow.transition('confirmed') def confirm(cls, records): cls.set_number(records) for rec in records: # FIXME check if does not exist previous occupancy if exist update state rec.update_folio('pending') @classmethod @ModelView.button def no_show(cls, records): for record in records: cls.write([record], {'registration_state': 'no_show'}) record.cancel_occupancy() @classmethod @ModelView.button def send_email(cls, records): for reserve in records: if reserve.state == 'confirmed': reserve.send_email_to() @classmethod @ModelView.button def bill(cls, records): for rec in records: cls.create_invoice(rec.lines) cls.check_finished(records) @classmethod @ModelView.button def bill_to_channel(cls, records): for rec in records: if rec.channel_invoice or not rec.channel or \ rec.channel.collection_mode != 'anticipated': continue cls.create_channel_invoice(rec) cls.create_channel_pay(rec) @classmethod def check_finished(cls, records): for rec in records: _folios, _charges = cls.pending_to_invoice(rec.lines) if not _folios and not _charges: cls.write([rec], {'state': 'finished'}) @classmethod def set_number(cls, bookings): """ Fill the number field with the booking sequence """ pool = Pool() Config = pool.get('hotel.configuration') config = Config.get_configuration() for booking in bookings: if booking.number or not config.booking_sequence: continue number = config.booking_sequence.get() cls.write([booking], {'number': number}) @classmethod def reconcile(cls, booking, voucher): pool = Pool() VoucherConfig = pool.get('account.voucher_configuration') MoveLine = pool.get('account.move.line') invoice = None config = VoucherConfig.get_configuration() account = config.customer_advance_account for folio in booking.lines: invoice = folio.invoice if invoice and invoice.state == 'paid': continue advances = [] payments = [] for voucher in booking.vouchers: for line in voucher.move.lines: if line.account.id == account.id and not line.reconciliation: advances.append(voucher) break elif invoice and line.account.id == invoice.account.id: payments.append(line) if invoice: to_reconcile_lines = [] if advances: invoice.create_move_advance(advances) invoice.save() if payments: invoice.add_payment_lines({invoice: payments}) invoice.save() # to_reconcile_lines.extend(payments) if invoice.amount_to_pay == Decimal(0): # invoice.paid([invoice]) for ml in invoice.payment_lines: if not ml.reconciliation: to_reconcile_lines.append(ml) for ml in invoice.move.lines: if not ml.reconciliation and ml.account.id == invoice.account.id: to_reconcile_lines.append(ml) if to_reconcile_lines: MoveLine.reconcile(to_reconcile_lines) @classmethod def get_grouped_invoices(cls, folios, charges): res = {} for fo in folios: if fo.booking.party: party = fo.booking.party else: party = fo.main_guest if not party: raise UserError(gettext('hotel.msg_customer_is_required')) bk = fo.booking agent_id = bk.channel.agent.id if bk.channel else None if party.id not in res.keys(): # Add room product to sale reference = '' if len(folios) == 1: reference = fo.registration_card else: reference = ','.join( [fo.registration_card for fo in folios] ) res[party.id] = { 'party': party, 'currency': bk.currency.id, 'payment_term': None, 'number': bk.number, 'guests_qty': len(fo.guests) + 1, 'reference': reference, 'agent': agent_id, 'rooms': fo.room.name, 'company': bk.company.id, 'price_list': bk.price_list.id if bk.price_list else None, 'add_default_charges': False, 'vouchers': bk.vouchers, 'lines': [{ 'folios': [fo], 'description': fo.get_room_info(), 'quantity': fo.nights_quantity, 'product': fo.product, 'unit_price': fo.unit_price, 'origin': str(bk), 'taxes_exception': bk.taxes_exception, }] } else: res[party.id]['rooms'] += ' ' + fo.room.name res[party.id]['lines'].append({ 'folios': [fo], 'description': fo.get_room_info(), 'quantity': fo.nights_quantity, 'product': fo.product, 'unit_price': fo.unit_price, 'origin': str(bk), 'taxes_exception': bk.taxes_exception, }) for charge in charges: bk = charge.folio.booking invoice_party_id = charge.invoice_to.id unit_price = bk.currency.round(charge.unit_price) if invoice_party_id not in res.keys(): res[invoice_party_id] = { 'party': charge.invoice_to, 'currency': bk.currency.id, 'payment_term': None, 'number': bk.number, 'reference': charge.folio.registration_card, 'lines': [], } res[invoice_party_id]['lines'].append({ 'description': ' | '.join([ str(charge.date_service), charge.order or '', charge.description or '' ]), 'quantity': charge.quantity, 'product': charge.product, 'unit_price': unit_price, 'charge': charge, 'origin': str(bk), 'taxes_exception': bk.taxes_exception, }) return res @classmethod def _get_invoice_line(cls, invoice, line, record=None): product = line['product'] new_line = { 'type': 'line', 'invoice': invoice.id, 'unit': product.template.default_uom.id, 'account': product.template.account_category.account_revenue_used.id, 'invoice_type': 'out', 'quantity': line['quantity'], 'unit_price': line['unit_price'], 'product': product.id, 'party': invoice.party.id, 'description': line['description'], 'origin': line['origin'], } if not line['taxes_exception']: taxes_ids = cls.get_taxes(line['product'], invoice.party, invoice.currency) if taxes_ids: new_line.update({'taxes': [('add', taxes_ids)]}) return new_line @classmethod def get_taxes(cls, product, party=None, currency=None): ctx = cls.get_context_price(product, party, currency) return ctx['taxes'] @classmethod def pending_to_invoice(cls, folios): _folios = [] _charges = [] for folio in folios: if not folio.invoice_line or not folio.to_invoice: _folios.append(folio) for charge in folio.charges: if not charge.invoice_line and charge.to_invoice: _charges.append(charge) return _folios, _charges @classmethod def create_channel_pay(cls, bk): Note = Pool().get('account.note') NoteLine = Pool().get('account.note.line') Invoice = Pool().get('account.invoice') Config = Pool().get('account.voucher_configuration') config = Config.get_configuration() lines_to_create = [] note, = Note.create([{ 'description': bk.number, 'journal': config.default_journal_note.id, 'date': date.today(), 'state': 'draft', }]) line1, = NoteLine.create([{ 'note': note.id, 'debit': 0, 'credit': bk.channel_commission, 'party': bk.channel.agent.party.id, 'account': bk.channel_invoice.account.id, 'description': bk.ota_booking_code, }]) line2, = NoteLine.create([{ 'note': note.id, 'debit': bk.channel_commission, 'credit': 0, 'party': bk.channel.agent.party.id, 'account': bk.channel.debit_account.id, 'description': bk.number, }]) Note.write([note], {'lines': [('add', [line1, line2])]}) Note.post([note]) lines_to_add = [] for line in note.move.lines: if line.account.id == bk.channel_invoice.account.id: lines_to_add.append(line.id) Invoice.write([bk.channel_invoice], { 'payment_lines': [('add', lines_to_add)], }) bk.channel_invoice.save() @classmethod def create_channel_invoice(cls, bk): pool = Pool() Invoice = pool.get('account.invoice') InvoiceLine = pool.get('account.invoice.line') Foilo = pool.get('hotel.folio') if not bk.channel: return data = { 'party': bk.channel.agent.party, 'reference': bk.number, 'description': f"{bk.ota_booking_code} | {bk.party.name}", 'payment_term': bk.payment_term, 'number': bk.number, } invoice = cls._get_new_invoice(data) invoice.on_change_invoice_type() invoice.save() for folio in bk.lines: if folio.invoice_line: continue _folio = { 'product': folio.product, 'quantity': folio.nights_quantity, 'unit_price': folio.unit_price, 'description': folio.room.name, 'origin': str(bk), 'taxes_exception': bk.taxes_exception, } line, = InvoiceLine.create([ cls._get_invoice_line(invoice, _folio) ]) Foilo.write([folio], {'invoice_line': line.id}) invoice.save() invoice.update_taxes([invoice]) cls.write([bk], {'channel_invoice': invoice.id}) Invoice.post([invoice]) @classmethod def create_invoice(cls, folios): pool = Pool() Configuration = pool.get('hotel.configuration') Folio = pool.get('hotel.folio') FolioCharge = pool.get('hotel.folio.charge') InvoiceLine = pool.get('account.invoice.line') config = Configuration.get_configuration() invoice = {} _folios, _charges = cls.pending_to_invoice(folios) if not _folios and not _charges: return invoice_to_create = cls.get_grouped_invoices(_folios, _charges) for rec in invoice_to_create.values(): invoice = cls._get_new_invoice(rec) invoice.on_change_invoice_type() invoice.save() # Add and create default charges lines if exists if rec.get('guests_qty') and rec.get('add_default_charges'): for product in config.default_charges: if rec['party']: taxes_ids = cls.get_taxes( product, rec['party'], invoice.currency ) new_line = { 'invoice': invoice.id, 'type': 'line', 'unit': product.template.default_uom.id, 'quantity': rec['guests_qty'], 'unit_price': product.template.list_price, 'product': product.id, 'description': product.rec_name, 'origin': str(self), } if taxes_ids: new_line.update({'taxes': [('add', taxes_ids)]}) if new_line: InvoiceLine.create([new_line]) for _line in rec['lines']: line, = InvoiceLine.create([ cls._get_invoice_line(invoice, _line) ]) if _line.get('folios'): Folio.write(_line.get('folios'), { 'invoice_line': line.id, }) else: FolioCharge.write([_line.get('charge')], { 'invoice_line': line.id, }) invoice.save() invoice.update_taxes([invoice]) @classmethod def _get_new_invoice(cls, data): pool = Pool() Invoice = pool.get('account.invoice') Party = pool.get('party.party') Agent = pool.get('commission.agent') Journal = pool.get('account.journal') PaymentTerm = pool.get('account.invoice.payment_term') Date = pool.get('ir.date') date_ = Date.today() price_list_id = None if data.get('price_list'): price_list_id = data['price_list'] company_id = Transaction().context.get('company') party = data['party'] if data.get('description'): description = data['description'] else: description = f"{data['number']} | {data.get('rooms')}" reference = data.get('reference') agent = None if data.get('agent'): agent = Agent(data['agent']) journal, = Journal.search([('type', '=', 'revenue')], limit=1) address = party.address_get(type='invoice') payment_term = data.get('payment_term', None) if not payment_term: payment_terms = PaymentTerm.search([]) payment_term = payment_terms[0] return Invoice( company=company_id, payment_term=payment_term.id, party=party.id, account=party.account_receivable_used.id, invoice_date=date_, description=description, state='draft', reference=reference, agent=agent, journal=journal, type='out', invoice_type='P', invoice_address=address.id, ) def update_folio(self, state): Line = Pool().get('hotel.folio') Line.write(list(self.lines), {'registration_state': state}) 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: raise UserError(gettext('hotel.msg_room_no_clean', s=hk.room.name)) @classmethod def get_context_price(cls, product, party=None, currency=None, _date=None, price_list=None, taxes_exception=False): context = {} if currency: context['currency'] = currency.id if party: context['customer'] = party.id if _date: context['sale_date'] = _date if price_list: context['price_list'] = price_list.id context['uom'] = product.template.default_uom.id # Set taxes before unit_price to have taxes in context of sale price taxes = [] pattern = {} if not taxes_exception and party: for tax in product.customer_taxes_used: if party and party.customer_tax_rule: tax_ids = party.customer_tax_rule.apply(tax, pattern) if tax_ids: taxes.extend(tax_ids) continue taxes.append(tax.id) if party and party.customer_tax_rule: tax_ids = party.customer_tax_rule.apply(None, pattern) if tax_ids: taxes.extend(tax_ids) context['taxes'] = taxes return context def get_total_advance(self, name): Advance = Pool().get('hotel.booking-account.voucher') vouchers = Advance.search([('booking', '=', self.id)]) res = sum([voucher.voucher.amount_to_pay for voucher in vouchers]) return res def get_pending_to_pay(self, name): if self.total_amount: return self.total_amount - (self.total_advance or 0) return 0 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_tax_amount(self, name): Tax = Pool().get('account.tax') Booking = Pool().get('hotel.booking') res = _ZERO for line in self.lines: taxes_ids = Booking.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_channel_commission(self, name): res = sum(line.commission_amount for line in self.lines if line.commission_amount) return res 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: raise UserError(gettext('El cliente no tiene un correo asociado')) @fields.depends('price_list', 'breakfast_included') def on_change_price_list(self): if self.price_list: self.breakfast_included = self.price_list.breakfast_included class BookingReport(Report): __name__ = 'hotel.booking' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) user = Pool().get('res.user')(Transaction().user) report_context['company'] = user.company return report_context class BookingStatementReport(Report): __name__ = 'hotel.booking_statement' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) user = Pool().get('res.user')(Transaction().user) report_context['company'] = user.company return report_context class SelectRoomsAsk(ModelView): 'Select Rooms Assistant' __name__ = 'hotel.booking.select_rooms.ask' arrival_date = fields.Date('Arrival Date', required=True) departure_date = fields.Date('Departure Date', required=True) accommodation = fields.Many2One('product.product', 'Accommodation', domain=[ ('template.kind', '=', 'accommodation'), ]) rooms = fields.Many2Many('hotel.room', None, None, 'Rooms', domain=[ ('id', 'in', Eval('targets')), ]) overbooking = fields.Boolean('Overbooking') targets = fields.Function(fields.Many2Many('hotel.room', None, None, 'Targets'), 'on_change_with_targets') unit_price = fields.Numeric('Unit Price', digits=(16, 4), required=True) @staticmethod def default_accommodation(): Configuration = Pool().get('hotel.configuration') config = Configuration.get_configuration() if config.default_accommodation: return config.default_accommodation.id @fields.depends('accommodation', 'departure_date', 'arrival_date') def on_change_with_unit_price(self): Booking = Pool().get('hotel.booking') booking = Booking(Transaction().context.get('active_id')) ctx = {} if booking.price_list: ctx['price_list'] = booking.price_list ctx['sale_date'] = self.arrival_date ctx['currency'] = booking.currency.id if booking.party: ctx['customer'] = booking.party.id if self.accommodation and self.departure_date and self.arrival_date: product = self.accommodation unit_price = product.template.list_price quantity = (self.departure_date - self.arrival_date).days if booking.price_list: with Transaction().set_context(ctx): unit_price = booking.price_list.compute( booking.party, product, unit_price, quantity, product.default_uom) unit_price = booking.currency.round(unit_price) return unit_price @fields.depends('arrival_date', 'departure_date', 'accommodation', 'overbooking') def on_change_with_targets(self, name=None): pool = Pool() RoomTemplate = pool.get('hotel.room-product.template') Room = pool.get('hotel.room') Folio = pool.get('hotel.folio') res = [] if not self.accommodation or not self.arrival_date or not self.departure_date: return res if self.overbooking: return [r.id for r in Room.search([])] room_templates = RoomTemplate.search([ ('template.accommodation_capacity', '>=', self.accommodation.accommodation_capacity) ]) rooms_ids = [t.room.id for t in room_templates] rooms_available_ids = Folio.get_available_rooms( self.arrival_date, self.departure_date, rooms_ids=rooms_ids ) return rooms_available_ids class SelectRooms(Wizard): 'Select Rooms' __name__ = 'hotel.booking.select_rooms' """ this is the wizard that allows the front desk employee to select rooms, based on the requirements listed by the customer. """ start = StateView('hotel.booking.select_rooms.ask', 'hotel.view_select_rooms_form', [ Button('Exit', 'end', 'tryton-cancel'), Button('Add and Continue', 'add_continue', 'tryton-forward'), Button('Add', 'add_rooms', 'tryton-ok'), ] ) add_rooms = StateTransition() add_continue = StateTransition() def transition_add_rooms(self): self._add_rooms() return 'end' def transition_add_continue(self): self._add_rooms() return 'start' def _add_rooms(self): pool = Pool() Line = pool.get('hotel.folio') Booking = pool.get('hotel.booking') booking = Booking(Transaction().context.get('active_id')) lines_to_create = [] product = self.start.accommodation for room in self.start.rooms: values = { 'booking': booking.id, 'product': product.id, 'reference': booking.ota_booking_code, 'contact': booking.contact, 'room': room.id, 'arrival_date': self.start.arrival_date, 'departure_date': self.start.departure_date, 'unit_price': self.start.unit_price, } if booking.party: values['main_guest'] = booking.party.id values.update({'product': product.id}) lines_to_create.append(values) Line.create(lines_to_create) booking.save() class BookingVoucher(ModelSQL): 'Booking - Voucher' __name__ = 'hotel.booking-account.voucher' _table = 'booking_vouchers_rel' booking = fields.Many2One('hotel.booking', 'Booking', ondelete='CASCADE', select=True, required=True) voucher = fields.Many2One('account.voucher', 'Voucher', select=True, domain=[('voucher_type', '=', 'receipt')], ondelete='RESTRICT', required=True) @classmethod def set_voucher_origin(cls, voucher_id, booking_id): cls.create([{ 'voucher': voucher_id, 'booking': booking_id, }]) class 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, header, data): report_context = super().get_context(records, header, data) MAX_DAYS = 30 pool = Pool() Company = pool.get('company.company') Room = pool.get('hotel.room') BookingFolio = pool.get('hotel.folio') 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 = BookingFolio.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 RoomsOccupancyStart(ModelView): 'Rooms Occupancy Start' __name__ = 'hotel.print_rooms_occupancy.start' date = fields.Date('Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) @staticmethod def default_date(): Date_ = Pool().get('ir.date') return Date_.today() @staticmethod def default_company(): return Transaction().context.get('company') class RoomsOccupancy(Wizard): 'Rooms Occupancy' __name__ = 'hotel.print_rooms_occupancy' start = StateView('hotel.print_rooms_occupancy.start', 'hotel.print_rooms_occupancy_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Open', 'print_', 'tryton-print', default=True), ]) print_ = StateReport('hotel.rooms_occupancy.report') def do_print_(self, action): company = self.start.company data = { 'ids': [], 'date': self.start.date, 'company': company.id, } return action, data def transition_print_(self): return 'end' class RoomsOccupancyReport(Report): __name__ = 'hotel.rooms_occupancy.report' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) pool = Pool() Company = pool.get('company.company') User = pool.get('res.user') Room = pool.get('hotel.room') Folio = pool.get('hotel.folio') start_date = data['date'] all_rooms = Room.search([], order=[('code', 'ASC')]) folios = Folio.search([ ('arrival_date', '<=', start_date), ('registration_state', '=', 'check_in'), ]) def _get_default_room(r): res = { 'room': r.name, 'guest': None, 'num_guest': None, 'party': None, 'arrival': None, 'departure': None, 'booking': None, 'registration_card': None, 'amount': 0, 'registration_state': None, } return res rooms_map = {room.id: _get_default_room(room) for room in all_rooms} occupancy_rooms = 0 for op in folios: rooms_map[op.room.id].update({ 'guest': op.main_guest.name, 'num_guest': len(op.guests), 'party': op.booking.party.name if op.booking.party else '', 'arrival': op.arrival_date, 'departure': op.departure_date, 'registration_card': op.registration_card, 'amount': op.total_amount, 'booking': op.booking.number, 'registration_state': op.registration_state, }) occupancy_rooms += 1 if all_rooms: occupancy_rate = (float(len(folios)) / len(all_rooms)) * 100 else: occupancy_rate = 0 report_context['records'] = rooms_map.values() report_context['occupancy_rate'] = occupancy_rate report_context['occupancy_rooms'] = occupancy_rooms report_context['company'] = Company(data['company']).party.name report_context['date'] = data['date'] report_context['user'] = User(Transaction().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, header, data): report_context = super().get_context(records, header, data) pool = Pool() Company = pool.get('company.company') Folio = pool.get('hotel.folio') records = Folio.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 class InvoicePaymentForm(ModelView): 'Invoice Payment Form' __name__ = 'invoice.payment.form' payment_mode = fields.Many2One('account.voucher.paymode', 'Payment Mode', domain=[], required=True) payment_amount = fields.Numeric('Payment amount', digits=(16, 2), required=True) party = fields.Many2One('party.party', 'Party', required=True) pay_date = fields.Date('Advance Date', required=True) reference = fields.Char('Reference') @classmethod def default_advance_date(cls): Date = Pool().get('ir.date') return Date.today() # class WizardInvoicePayment(Wizard): # 'Wizard Invoice Payment' # __name__ = 'invoice.payment' # start = StateView('invoice.payment.form', # 'hotel.invoice_payment_view_form', [ # Button('Cancel', 'end', 'tryton-cancel'), # Button('Pay', 'pay_', 'tryton-ok', default=True), # ]) # pay_ = StateTransition() # # @classmethod # def __setup__(cls): # super(WizardInvoicePayment, cls).__setup__() # # def default_start(self, fields): # pool = Pool() # Booking = pool.get('hotel.booking') # Date = pool.get('ir.date') # booking = Booking(Transaction().context['active_id']) # return { # 'payment_amount': booking.pending_to_pay, # 'party': booking.party.id, # 'pay_date': Date.today(), # } # # def transition_pay_(self): # pool = Pool() # Booking = pool.get('hotel.booking') # Invoice = pool.get('account.invoice') # active_id = Transaction().context.get('active_id', False) # booking = Booking(active_id) # # invoices = [] # for folio in booking.lines: # inv = folio.invoice # if inv.state != 'posted': # continue # invoices.append(inv) # if not invoices: # return 'end' # # invoice = invoices[0] # invoice.create_move_advance(booking.vouchers) # # # if not sale.number: # # Invoice.set_number([sale]) # # if not invoice.party.account_receivable: # # raise PartyMissingAccount( # # gettext('sale_pos.msg_party_without_account_receivable', s=sale.party.name)) # pass # account = invoice.party.account_receivable.id # # if form.payment_amount: # payment = StatementLine( # statement=statements[0].id, # date=date.today(), # amount=form.payment_amount, # party=sale.party.id, # account=account, # description=self.start.voucher, # sale=active_id, # # number=self.start.voucher, # # voucher=self.start.voucher, # ) # payment.save() # # if sale.total_amount != sale.paid_amount: # return 'start' # sale.save() # if self.start.do_invoice: # # for inv in sale.invoices: # # if inv.state == 'posted': # # inv.write([inv], {'state': 'draft'}) # Invoice.workflow_to_end([sale]) # return 'end'