# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from datetime import datetime, timedelta, date from decimal import Decimal from trytond.model import Workflow, ModelView, ModelSQL, fields from trytond.wizard import ( Wizard, StateView, Button, StateTransition, StateReport ) from trytond.report import Report from trytond.pyson import Eval, If, In, Get, Bool from trytond.transaction import Transaction from trytond.pool import Pool from trytond.exceptions import UserError from trytond.i18n import gettext from trytond.modules.account.tax import TaxableMixin from .constants import ( STATE_BOOKING, REASON, GUARANTEE, SATISFACTION, MEDIA, PLAN, COMPLEMENTARY, PAYMENT_METHOD_CHANNEL, TYPE_DOCUMENT ) SEPARATOR = ' | ' _ZERO = Decimal('0.0') STATES = { 'readonly': Eval('state') != 'offer', } NO_CHANGES = ['finished', 'cancelled', 'not_show'] STATES_BLOCKED = { 'readonly': Eval('state').in_(NO_CHANGES), } STATES_CONFIRMED = { 'readonly': Eval('state') != 'offer', 'required': Eval('state') == 'confirmed', } class Booking(Workflow, ModelSQL, ModelView): 'Booking' __name__ = 'hotel.booking' _rec_name = 'number' number = fields.Char('Number', readonly=True, select=True, help="Sequence of reservation.") party = fields.Many2One('party.party', 'Customer', required=False, select=True, states=STATES_BLOCKED, help="Person or company owner of the booking.") contact = fields.Char('Contact', states=STATES_BLOCKED, help='Main contact or person how request booking') payment_term = fields.Many2One('account.invoice.payment_term', 'Payment Term', states=STATES_BLOCKED) booking_date = fields.DateTime('Booking Date', readonly=True) guests_num = fields.Function(fields.Integer('Person Number'), 'get_person_num') adult_num = fields.Function(fields.Integer('Adult Number'), 'get_person_num') children_num = fields.Function(fields.Integer('Children Number'), 'get_person_num') group = fields.Boolean('Group', states=STATES_BLOCKED) complementary = fields.Boolean('Complementary', states=STATES_BLOCKED) type_complementary = fields.Selection(COMPLEMENTARY, 'Type Complementary', states=STATES_BLOCKED) channel = fields.Many2One('hotel.channel', 'Channel', states={ 'invisible': Eval('media') != 'ota', 'readonly': Eval('state').in_(['finished', 'cancelled']), }, help="Agency or channel that do reservation.") state = fields.Selection(STATE_BOOKING, 'State', readonly=True, required=True) state_string = state.translated('state') price_list = fields.Many2One('product.price_list', 'Price List', states={ 'readonly': Bool(Eval('lines')), }, depends=['state', 'company', 'lines']) company = fields.Many2One('company.company', 'Company', required=True, states=STATES, domain=[('id', If(In('company', Eval('context', {})), '=', '!='), Get(Eval('context', {}), 'company', 0))], readonly=True) lines = fields.One2Many('hotel.folio', 'booking', 'Folios', states={ 'required': Eval('state') == 'confirmed', 'readonly': Eval('state').in_(['finished', 'cancelled']), }, depends=['state', 'party'], context={'party': Eval('party')}) cancellation_policy = fields.Many2One('hotel.policy.cancellation', 'Cancellation Policy', states=STATES_BLOCKED) currency = fields.Many2One('currency.currency', 'Currency', required=True, states={ 'readonly': (Eval('state') != 'offer') | (Eval('lines', [0]) & Eval('currency', 0)), }, depends=['state']) satisfaction = fields.Selection(SATISFACTION, 'Satisfaction') media = fields.Selection(MEDIA, 'Media', states=STATES_BLOCKED, help="Way through which the booking arrives.") media_string = media.translated('media') plan = fields.Selection(PLAN, 'Commercial Plan', states=STATES_BLOCKED, help="Plans offered by hotel and selected by guest for booking.") plan_string = plan.translated('plan') comments = fields.Text('Comments', states=STATES_BLOCKED) reason = fields.Selection(REASON, 'Tourism Segment', states=STATES_BLOCKED) reason_string = reason.translated('segment') guarantee = fields.Selection(GUARANTEE, 'Guarantee', states=STATES_BLOCKED) guarantee_string = guarantee.translated('guarantee') untaxed_amount = fields.Function(fields.Numeric('Untaxed Amount', digits=(16, 2), depends=['lines']), 'get_untaxed_amount') tax_amount = fields.Function(fields.Numeric('Tax Amount', digits=(16, 2), depends=['lines']), 'get_tax_amount') total_amount = fields.Function(fields.Numeric('Total Amount', digits=(16, 2), depends=['lines']), 'get_total_amount') collect_amount = fields.Function(fields.Numeric('Collect Amount', digits=(16, 2), depends=['lines']), 'get_collect_amount') code = fields.Char('Code', states={'readonly': True}) booker = fields.Many2One('party.party', 'Booker', states={'readonly': True}) created_channel = fields.DateTime('Created Channel', states={'readonly': True}) vouchers = fields.Many2Many('hotel.booking-account.voucher', 'booking', 'voucher', 'Vouchers', states={'readonly': False}) ota_booking_code = fields.Char('OTA Code', select=True, states={'invisible': Eval('media') != 'ota'} ) payments = fields.One2Many('account.statement.line', 'source', 'Payments', states=STATES_BLOCKED, readonly=True) vehicles_num = fields.Integer('Vehicles Number', states=STATES_BLOCKED, help="Number of vehicles that bring with guests.") travel_cause = fields.Char('Travel Cause', states=STATES_BLOCKED) taxes_exception = fields.Boolean('Taxes Exception', states=STATES_BLOCKED) total_advances = fields.Function(fields.Numeric('Total Advance', digits=(16, 2)), 'get_total_advances') pending_to_pay = fields.Function(fields.Numeric('Pending to Pay', digits=(16, 2)), 'get_pending_to_pay') breakfast_included = fields.Boolean('Breakfast Included', states=STATES_BLOCKED) channel_commission = fields.Function(fields.Numeric('Channel Commission', digits=(16, 2), depends=['lines']), 'get_channel_commission') commission = fields.Many2One('commission', 'Commission') channel_invoice = fields.Many2One('account.invoice', 'Channel Invoice', states={ 'invisible': ~Eval('channel'), 'readonly': True }) channel_payment_method = fields.Selection(PAYMENT_METHOD_CHANNEL, 'Channel Payment Method', states={'invisible': ~Eval('channel')}, depends=['channel'] ) invoices = fields.Function(fields.Many2Many('account.invoice', None, None, 'Invoices'), 'get_invoices') extra_commissions = fields.Many2Many('hotel.booking-channel.commission', 'booking', 'commission', 'Channel Commission', domain=[ ('channel', '=', Eval('channel')) ], states=STATES_BLOCKED) stock_moves = fields.Function(fields.One2Many('stock.move', 'origin', 'Moves', readonly=True), 'get_stock_moves') channel_icon = fields.Function(fields.Char('Channel Icon'), 'get_channel_icon') emails = fields.One2Many('email.activity', 'origin', 'Emails', readonly=True) responsible_payment = fields.Selection([ ('holder', 'Holder'), ('holder_guest', 'Holder / Guest'), ('guest', 'Guest'), ], 'Responsible for Payment', states={ 'required': True, 'readonly': Eval('state').in_(NO_CHANGES), }) credit_card = fields.Char('Credit Card', states=STATES_BLOCKED) vip = fields.Boolean('V.I.P. Customer', states=STATES_BLOCKED) pending_acco = fields.Function(fields.Numeric('Pending to Pay', digits=(16, 2)), 'get_pending_to_pay') pending_charges = fields.Function(fields.Numeric('Pending to Pay', digits=(16, 2)), 'get_pending_to_pay') income_moves = fields.One2Many('account.move', 'origin', 'Income Moves', states={ 'readonly': Eval('state').in_(['finished', 'cancelled']), }) space = fields.Many2One('analytic_account.space', 'Space', states=STATES_BLOCKED) collection_mode = fields.Selection([ ('', ''), ('anticipated', 'Anticipated'), ('post_checkin', 'Post Checkin'), ], 'Collection Mode', required=False, help="Commission collection mode") @classmethod def __setup__(cls): super(Booking, cls).__setup__() cls._order.insert(0, ('create_date', 'DESC')) cls._transitions |= set(( ('offer', 'confirmed'), ('offer', 'cancelled'), ('confirmed', 'offer'), ('cancelled', 'offer'), ('confirmed', 'cancelled'), ('confirmed', 'not_show'), ('confirmed', 'finished'), ('not_show', 'confirmed'), )) cls._buttons.update({ 'select_rooms': { 'invisible': Eval('state').in_(NO_CHANGES), }, 'update_holder': { 'invisible': Eval('state').in_(NO_CHANGES), }, 'cancel': { 'invisible': Eval('state').in_(NO_CHANGES) }, 'offer': { 'invisible': Eval('state').in_(['offer', 'finished']) }, 'confirm': { 'invisible': ~Eval('state').in_(['offer', 'not_show']) }, 'not_show': { 'invisible': Eval('state') != 'confirmed', }, 'do_payment': { 'invisible': Eval('state').in_(NO_CHANGES), }, 'send_email_booking': { 'invisible': Eval('state').in_(['finished']) }, 'send_email_checkin': { 'invisible': Eval('state').in_(['finished']) }, 'bill': { 'invisible': ~Eval('state').in_(['confirmed', 'not_show']), }, }) @staticmethod def default_responsible_payment(): return 'holder' @staticmethod def default_collection_mode(): return 'post_checkin' @staticmethod def default_space(): Config = Pool().get('hotel.configuration') config = Config.get_configuration() if config.space_booking: return config.space_booking.id @staticmethod def default_payment_term(): Config = Pool().get('hotel.configuration') config = Config.get_configuration() if config.payment_term: return config.payment_term.id def get_channel_icon(self, name): name_icon = 'hotel-channel-house' if self.channel and self.channel.code: name_icon = f'hotel-channel-{self.channel.code}' return name_icon @classmethod def trigger_create(cls, records): cls.set_number(records) @classmethod def delete(cls, records): for record in records: if record.number: raise UserError(gettext('hotel.msg_can_no_delete_booking')) super(Booking, cls).delete(records) @classmethod def copy(cls, bookings, default=None): if default is None: default = {} default = default.copy() default['number'] = None default['booking_date'] = datetime.now() default['payments'] = None return super(Booking, cls).copy(bookings, default=default) def get_stock_moves(self, name=None): moves = [] for folio in self.lines: for charge in folio.charges: if charge.move: moves.append(charge.move.id) return moves @classmethod def search_rec_name(cls, name, clause): _, operator, value = clause if operator.startswith('!') or operator.startswith('not '): bool_op = 'AND' else: bool_op = 'OR' domain = [ bool_op, ('number', operator, value), ('ota_booking_code', operator, value), ('party.name', operator, value), ('contact', operator, value), ] return domain @staticmethod def default_currency(): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: return Company(company).currency.id @staticmethod def default_company(): return Transaction().context.get('company') or False @staticmethod def default_state(): return 'offer' @staticmethod def default_plan(): return 'bed_breakfast' @staticmethod def default_booking_date(): now = datetime.now() return now def _round_taxes(self, taxes): if not self.currency: return for taxline in taxes.values(): taxline['amount'] = self.currency.round(taxline['amount']) def _get_taxes(self): pool = Pool() Tax = pool.get('account.tax') Configuration = pool.get('account.configuration') taxes = {} with Transaction().set_context({}): config = Configuration(1) tax_rounding = config.get_multivalue('tax_rounding') def compute(_taxes, unit_price, quantity): l_taxes = Tax.compute(Tax.browse(_taxes), unit_price, quantity) for tax in l_taxes: taxline = TaxableMixin._compute_tax_line(**tax) # Base must always be rounded per folio as there will # be one tax folio per taxable_lines if self.currency: taxline['base'] = self.currency.round(taxline['base']) if taxline not in taxes: taxes[taxline] = taxline else: taxes[taxline]['base'] += taxline['base'] taxes[taxline]['amount'] += taxline['amount'] if tax_rounding == 'line': self._round_taxes(taxes) for folio in self.lines: compute(folio.taxes, folio.unit_price, folio.nights_quantity) for charge in folio.charges: compute(folio.taxes, charge.unit_price, charge.quantity) if tax_rounding == 'document': self._round_taxes(taxes) return taxes def get_invoices(self, name=None): res = [] for folio in self.lines: if folio.invoice_line: res.append(folio.invoice_line.invoice.id) for charge in folio.charges: if charge.invoice_line: res.append(charge.invoice_line.invoice.id) return list(set(res)) def get_person_num(self, name): res = { 'guests_num': [], 'adult_num': [], 'children_num': [], } for line in self.lines: res['guests_num'].append(line.num_adults + line.num_children) if name == 'adult_num' and line.num_adults: res[name].append(line.num_adults) elif name == 'children_num' and line.num_children: res[name].append(line.num_children) return sum(res[name]) @fields.depends('ota_booking_code', 'lines') def on_change_ota_booking_code(self): if self.ota_booking_code: for line in self.lines: line.reference = self.ota_booking_code def is_person(self): if self.party.type_document in ( '12', '13', '21', '22', '41', '42', '47'): return True @fields.depends('party', 'price_list', 'lines') def on_change_party(self): if self.party: if self.party.sale_price_list: self.price_list = self.party.sale_price_list.id self.price_list.rec_name = self.party.sale_price_list.rec_name for folio in self.lines: if self.is_person(): folio.main_guest = self.party.id @fields.depends('channel') def on_change_channel(self): if self.channel: if self.channel.invoice_to == 'channel': self.party = self.channel.agent.party.id self.responsible_payment = 'holder_guest' if self.channel.payment_method == 'ota_collect': self.responsible_payment = 'holder_guest' self.channel_payment_method = self.channel.payment_method self.price_list = self.channel.price_list self.collection_mode = self.channel.collection_mode else: self.channel_payment_method = None self.price_list = None self.collection_mode = None @classmethod @ModelView.button_action('hotel.wizard_select_rooms') def select_rooms(cls, records): pass @classmethod @ModelView.button_action('hotel.wizard_update_holder') def update_holder(cls, records): pass @classmethod @ModelView.button_action('hotel.wizard_statement_payment_form') def do_payment(cls, records): pass @classmethod @ModelView.button @Workflow.transition('offer') def offer(cls, records): pass @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, records): for rec in records: for folio in rec.lines: if folio.registration_state in ['check_in', 'check_out']: raise UserError(gettext('hotel.msg_no_delete_folios')) else: folio.delete([folio]) @classmethod @ModelView.button @Workflow.transition('not_show') def not_show(cls, records): pass @classmethod @ModelView.button @Workflow.transition('confirmed') def confirm(cls, records): for rec in records: rec.update_folio('pending') rec.state = 'confirmed' rec.save() rec.send_email('booking_email') @classmethod @ModelView.button def no_show(cls, records): for record in records: cls.write([record], {'state': 'no_show'}) @classmethod @ModelView.button def send_email_booking(cls, records): for booking in records: if booking.state == 'confirmed': booking.send_email('booking_email') @classmethod @ModelView.button_action('hotel.wizard_bill_booking') def bill(cls, records): pass @classmethod def create_channel_voucher(cls, bk): Voucher = Pool().get('account.voucher') # If ota_collect is paymode for folios if not bk.channel or bk.responsible_payment != 'ota_collect': return payment_mode = bk.channel.payment_mode if not payment_mode: raise UserError(gettext('hotel.msg_missing_payment_mode')) party = bk.channel.agent.party account_id = Voucher.get_account('receipt', payment_mode) amount = bk.collect_amount - bk.channel_commission lines = [{ 'detail': f'OTA Code {bk.ota_booking_code}', 'amount': amount, 'amount_original': amount, 'account': party.account_receivable.id, }] to_create = { 'party': party.id, 'voucher_type': 'receipt', 'date': date.today(), 'description': f'Booking {bk.number}', 'payment_mode': payment_mode.id, 'account': account_id, 'journal': payment_mode.journal.id, 'lines': [('create', lines)], 'amount_to_pay': amount, 'method_counterpart': 'one_line', # 'state': 'draft', } voucher, = Voucher.create([to_create]) Voucher.process([voucher]) cls.write([bk], {'vouchers': [('add', [voucher])]}) @classmethod def concile_charges(cls, records): ''' We need mark charges pending to pay as paid, if the customer already paid all booking charges ''' for booking in records: if booking.pending_to_pay != 0: continue for folio in booking.lines: for charge in folio.charges: if charge.status == 'pending': charge.status = 'paid' charge.save() @classmethod def check_finished(cls, records): for rec in records: _folios, _charges = cls.pending_to_invoice(rec.lines) if not _folios and not _charges: cls.write([rec], {'state': 'finished'}) @classmethod def set_number(cls, bookings): """ Fill the number field with the booking sequence """ pool = Pool() Config = pool.get('hotel.configuration') config = Config.get_configuration() for booking in bookings: if booking.number or not config.booking_sequence: continue number = config.booking_sequence.get() cls.write([booking], {'number': number}) @classmethod def reconcile(cls, booking, voucher): pool = Pool() VoucherConfig = pool.get('account.voucher_configuration') MoveLine = pool.get('account.move.line') invoice = None config = VoucherConfig.get_configuration() account = config.customer_advance_account for folio in booking.lines: invoice = folio.invoice if invoice and invoice.state == 'paid': continue advances = [] payments = [] for voucher in booking.vouchers: for line in voucher.move.lines: if line.account.id == account.id and not line.reconciliation: advances.append(voucher) break elif invoice and line.account.id == invoice.account.id: payments.append(line) if invoice: to_reconcile_lines = [] if advances: invoice.create_move_advance(advances) invoice.save() if payments: invoice.add_payment_lines({invoice: payments}) invoice.save() if invoice.amount_to_pay == Decimal(0): for ml in invoice.payment_lines: if not ml.reconciliation: to_reconcile_lines.append(ml) for ml in invoice.move.lines: if not ml.reconciliation and ml.account.id == invoice.account.id: to_reconcile_lines.append(ml) if to_reconcile_lines: MoveLine.reconcile(to_reconcile_lines) @classmethod def _get_charge_line(cls, cha, bk): description = cha.product.template.name if cha.order: description.append(cha.order) if cha.kind == 'accommodation': description = cha.description + ' | ' + str(cha.date_service) # 'description': SEPARATOR.join(description), return { 'description': description, 'quantity': cha.quantity, 'product': cha.product, 'unit_price': bk.currency.round(cha.unit_price), 'charge': cha, 'origin': str(cha.folio), 'taxes': cha.folio.taxes, 'taxes_exception': bk.taxes_exception, 'analytic_account': cha.analytic_account, } @classmethod def _get_accommodation_line(cls, fo, bk): return { 'folios': [fo], 'description': fo.get_room_info(), 'quantity': fo.nights_quantity, 'product': fo.product, 'unit_price': fo.unit_price, 'taxes': fo.taxes, 'origin': str(fo), 'taxes_exception': bk.taxes_exception, } @classmethod def add_invoice_charges(cls, kind, charge, res): # folio = charge.folio # bk = folio.booking # if kind == 'only_products' and charge.kind == 'accommodation': # return # if kind == 'only_accommodation' and charge.kind == 'product': # return # # if bk.responsible_payment == 'holder': # party = bk.party # elif bk.responsible_payment == 'guest': # party = folio.main_guest # else: # if charge.kind == 'accommodation': # party = bk.party # else: # party = folio.main_guest # # if party.id not in res.keys(): # res[party.id] = { # 'party': party, # 'currency': bk.currency.id, # 'payment_term': bk.payment_term, # 'number': bk.number, # 'reference': folio.registration_card, # 'lines': [], # } # # line = cls._get_charge_line(cha, bk) # res[party.id]['lines'].append(line) pass @classmethod def get_grouped_invoices(cls, folios, kind=None, party=None): res = {} for folio in folios: accomodations = {} bk = folio.booking price_list = bk.price_list agent_id = bk.channel.agent.id if bk.channel else None for charge in folio.charges: if charge.invoice_line: continue if kind == 'only_products' and charge.kind == 'accommodation': continue if kind == 'only_accommodation' and charge.kind == 'product': continue if bk.responsible_payment == 'holder': party = bk.party elif bk.responsible_payment == 'guest': party = folio.main_guest else: if charge.kind == 'accommodation': party = bk.party else: party = folio.main_guest if party.id not in res.keys(): res[party.id] = { 'party': party, 'currency': bk.currency.id, 'payment_term': bk.payment_term, 'number': bk.number, 'reference': [folio.registration_card], 'rooms': [folio.room.name], 'price_list': price_list.id if price_list else None, 'company': bk.company.id, 'lines': [], } if charge.kind == 'accommodation' and agent_id: res[party.id]['agent'] = agent_id res[party.id]['ota_booking_code'] = bk.ota_booking_code or '' res[party.id]['guests_qty'] = len(folio.guests) line = cls._get_charge_line(charge, bk) res[party.id]['lines'].append(line) return res @classmethod def _get_invoice_line(cls, invoice, line, record=None): product = line['product'] new_line = { 'type': 'line', 'invoice': invoice.id, 'unit': product.template.default_uom.id, 'account': product.template.account_category.account_revenue_used.id, 'invoice_type': 'out', 'quantity': line['quantity'], 'unit_price': line['unit_price'], 'product': product.id, 'party': invoice.party.id, 'description': line['description'], 'taxes': [('add', line['taxes'])], 'origin': line['origin'], } analytic_account = line.get('analytic_account', None) if analytic_account: new_line['analytic_accounts'] = [('create', [{ 'account': analytic_account.id, 'root': analytic_account.root.id, }])] return new_line def do_moves(self): Move = Pool().get('stock.move') moves = {} to_create = [] for folio in self.lines: for charge in folio.charges: move = self.get_move(charge) if move: moves[folio.id] = move for m in moves: moves[m].state = 'draft' to_create.append(moves[m]._save_values) Move.create(to_create) Move.do(self.moves) def get_move(self, charge): ''' Return stock move for charge according a storage ''' pool = Pool() Move = pool.get('stock.move') product = self.product if not product or product.type != 'goods' and self.quantity <= 0: return if not charge.folio.storage: return None customer_location = self.party.customer_location move = Move() move.quantity = self.quantity move.uom = product.default_uom move.product = product # Add on create charge take default storage # from folio if isn not present move.from_location = charge.folio.storage move.to_location = customer_location move.state = 'draft' move.company = self.company move.unit_price = self.unit_price move.currency = self.company.currency move.planned_date = self.date_service move.origin = charge return move @classmethod def get_taxes(cls, product, party=None, currency=None): ctx = cls.get_context_price(product, party, currency) return ctx['taxes'] @classmethod def pending_to_invoice(cls, folios): _folios = [] _charges = [] for folio in folios: if not folio.invoice_line or not folio.to_invoice: _folios.append(folio) for charge in folio.charges: if not charge.invoice_line and charge.to_invoice: _charges.append(charge) return _folios, _charges @classmethod def create_channel_pay(cls, bk): pool = Pool() Note = pool.get('account.note') NoteLine = pool.get('account.note.line') Invoice = pool.get('account.invoice') Config = pool.get('account.voucher_configuration') config = Config.get_configuration() commission = bk.currency.round(bk.channel_commission) note, = Note.create([{ 'description': bk.number, 'journal': config.default_journal_note.id, 'date': date.today(), 'state': 'draft', }]) line1, = NoteLine.create([{ 'note': note.id, 'debit': 0, 'credit': commission, 'party': bk.channel.agent.party.id, 'account': bk.channel_invoice.account.id, 'description': bk.ota_booking_code, }]) line2, = NoteLine.create([{ 'note': note.id, 'debit': commission, 'credit': 0, 'party': bk.channel.agent.party.id, 'account': bk.channel.debit_account.id, 'description': bk.number, }]) Note.write([note], {'lines': [('add', [line1, line2])]}) Note.post([note]) lines_to_add = [] for line in note.move.lines: if line.account.id == bk.channel_invoice.account.id: lines_to_add.append(line.id) Invoice.write([bk.channel_invoice], { 'payment_lines': [('add', lines_to_add)], }) bk.channel_invoice.save() @classmethod def create_channel_invoice(cls, bk): pool = Pool() Invoice = pool.get('account.invoice') InvoiceLine = pool.get('account.invoice.line') Folio = pool.get('hotel.folio') if not bk.channel: return data = { 'party': bk.channel.agent.party, 'reference': bk.number, 'description': f"{bk.ota_booking_code} | {bk.party.name}", 'payment_term': bk.payment_term, 'number': bk.number, } invoice = cls._get_new_invoice(data) invoice.on_change_invoice_type() invoice.save() for folio in bk.lines: if folio.invoice_line: continue _folio = { 'product': folio.product, 'quantity': folio.nights_quantity, 'unit_price': folio.unit_price, 'description': folio.room.name, 'origin': str(folio), 'taxes_exception': bk.taxes_exception, } line, = InvoiceLine.create([ cls._get_invoice_line(invoice, _folio) ]) Folio.write([folio], {'invoice_line': line.id}) invoice.save() invoice.update_taxes([invoice]) cls.write([bk], {'channel_invoice': invoice.id}) Invoice.validate([invoice]) invoice.save() invoice, = Invoice.browse([invoice.id]) try: Invoice.submit([invoice]) invoice.save() except: pass try: invoice = Invoice(invoice.id) Invoice.post(invoices) except: pass @classmethod def create_invoice(cls, folios, kind=None, party=None): pool = Pool() for folio in folios: folio.add_charge_occupancy(ctx='invoice') FolioCharge = pool.get('hotel.folio.charge') InvoiceLine = pool.get('account.invoice.line') invoice = {} invoice_to_create = cls.get_grouped_invoices(folios, kind, party) if not invoice_to_create: return for rec in invoice_to_create.values(): invoice = cls._get_new_invoice(rec) invoice.on_change_invoice_type() invoice.save() for _line in rec['lines']: line, = InvoiceLine.create([ cls._get_invoice_line(invoice, _line) ]) FolioCharge.write([_line.get('charge')], { 'invoice_line': line.id, }) invoice.save() invoice.update_taxes([invoice]) @classmethod def _get_new_invoice(cls, data): pool = Pool() Invoice = pool.get('account.invoice') Agent = pool.get('commission.agent') Journal = pool.get('account.journal') PaymentTerm = pool.get('account.invoice.payment_term') Date = pool.get('ir.date') date_ = Date.today() party = data['party'] ota_code = data.get('ota_booking_code', '') description = [] if data.get('description'): description.extend(data['description']) else: description.append(data['number']) if ota_code: description.append(ota_code) if data.get('rooms'): description.extend(data.get('rooms')) description = SEPARATOR.join(description) reference = SEPARATOR.join(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=data['company'], payment_term=payment_term.id, party=party.id, account=party.account_receivable_used.id, invoice_date=date_, description=description, state='draft', reference=reference, agent=agent, journal=journal, type='out', invoice_type='1', invoice_address=address.id, ) def update_folio(self, state): Folio = Pool().get('hotel.folio') Folio.write(list(self.lines), {'registration_state': state}) @classmethod def add_charges_occupancy(cls, bookings): for booking in bookings: for folio in booking.lines: folio.add_charge_occupancy() @classmethod def do_revenue_accounting(cls, bookings): pool = Pool() Date = pool.get('ir.date') Move = pool.get('account.move') MoveLine = pool.get('account.move.line') Journal = pool.get('account.journal') Period = pool.get('account.period') Config = pool.get('hotel.configuration') journal, = Journal.search([ ('type', '=', 'revenue'), ], limit=1) config = Config.get_configuration() today = Date.today() for bk in bookings: to_create = [] period_id = Period.find(bk.company.id, date=today) _move, = Move.create([{ 'date': today, 'journal': journal.id, 'period': period_id, 'origin': str(bk), 'state': 'draft', 'description': '', }]) # lines = [] debit = 0 for folio in bk.lines: reference = folio.registration_card for charge in folio.charges: if charge.line_move: continue product = charge.product account_id = product.account_revenue_used.id credit = charge.amount debit += credit desc = f'{product.rec_name} | {str(charge.date_service)}' _line = { 'description': desc, 'party': bk.party.id, 'debit': 0, 'credit': credit, 'reference': reference, 'account': account_id, 'move': _move.id # 'origin': str(self), } # lines.append(_line) mline, = MoveLine.create([_line]) charge.line_move = mline.id charge.save() if not config.recognise_account: raise UserError(gettext('hotel.msg_missing_recognise_account')) recognise_account_id = config.recognise_account.id move_line, = MoveLine.create([{ 'description': bk.number, 'party': bk.party.id, 'debit': debit, 'credit': 0, 'reference': reference, 'account': recognise_account_id, 'move': _move.id # 'origin': str(self), }]) charge.line_move = move_line.id charge.save() Move.post([_move]) @classmethod def get_context_price( cls, product, party=None, currency=None, _date=None, price_list=None, taxes_exception=False ): context = {} if currency: context['currency'] = currency.id if party: context['customer'] = party.id if _date: context['sale_date'] = _date if price_list: context['price_list'] = price_list.id context['uom'] = product.template.default_uom.id # Set taxes before unit_price to have taxes in context of sale price taxes = [] pattern = {} if not taxes_exception and party: for tax in product.customer_taxes_used: if party and party.customer_tax_rule: tax_ids = party.customer_tax_rule.apply(tax, pattern) if tax_ids: taxes.extend(tax_ids) continue taxes.append(tax.id) if party and party.customer_tax_rule: tax_ids = party.customer_tax_rule.apply(None, pattern) if tax_ids: taxes.extend(tax_ids) context['taxes'] = taxes return context def get_total_advances(self, name): Advance = Pool().get('hotel.booking-account.voucher') advances = Advance.search([('booking', '=', self.id)]) vouchers = sum([ad.voucher.amount_to_pay for ad in advances]) payments = sum([pay.amount for pay in self.payments]) folios_payments = sum([fol.total_advances for fol in self.lines]) return vouchers + payments + folios_payments def get_pending_to_pay(self, name): res = 0 if name == 'pending_to_pay': if self.total_amount: print('total_amount...', self.total_amount, self.total_advances) res = self.total_amount - (self.total_advances or 0) elif name == 'pending_acco': res = [fol.pending_accommodation for fol in self.lines] elif name == 'pending_charges': res = [fol.pending_charges for fol in self.lines] return res def get_collect_amount(self, name): return sum(folio.room_amount for folio in self.lines) 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 folio 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)) taxes_computed = self._get_taxes() res = sum([t['amount'] for t in taxes_computed], _ZERO) return res def get_untaxed_amount(self, name): res = _ZERO for folio in self.lines: res += folio.total_amount return res def get_channel_commission(self, name): res = [line.commission_amount for line in self.lines if line.commission_amount] base_comm = [folio.on_change_with_room_amount() for folio in self.lines] for comm in self.extra_commissions: extras = sum(base_comm) * Decimal(comm.commission / 100) res.append(extras) return self.currency.round(sum(res)) def send_email(self, name): pool = Pool() config = pool.get('hotel.configuration')(1) Template = pool.get('email.template') Activity = pool.get('email.activity') email = self.party.email template = getattr(config, name) if not template: return template.subject = f'{template.subject} No. {self.number}' if email: Template.send(template, self, email, attach=True) Activity.create([{ 'template': template.id, 'origin': str(self), 'status': 'sended', }]) else: raise UserError(gettext('hotel.msg_missing_party_email')) @fields.depends('price_list', 'breakfast_included') def on_change_price_list(self): if self.price_list: self.breakfast_included = self.price_list.breakfast_included @classmethod @ModelView.button def send_email_checkin(cls, records): for booking in records: if booking.state == 'confirmed': booking.send_email('check_in_email') @classmethod @ModelView.button def send_email_customer_experience(cls, records): for booking in records: if booking.state == 'confirmed': booking.send_email('customer_experience') class BookingReport(Report): __name__ = 'hotel.booking' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) user = Pool().get('res.user')(Transaction().user) report_context['company'] = user.company return report_context class BookingStatementReport(Report): __name__ = 'hotel.booking_statement' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) user = Pool().get('res.user')(Transaction().user) report_context['company'] = user.company return report_context class SelectRoomsAsk(ModelView): 'Select Rooms Assistant' __name__ = 'hotel.booking.select_rooms.ask' arrival_date = fields.Date('Arrival Date', required=True) departure_date = fields.Date('Departure Date', required=True) accommodation = fields.Many2One('product.product', 'Accommodation', domain=[ ('template.kind', '=', 'accommodation'), ]) rooms = fields.Many2Many('hotel.room', None, None, 'Rooms', domain=[ ('id', 'in', Eval('targets')), ]) overbooking = fields.Boolean('Overbooking') targets = fields.Function(fields.Many2Many('hotel.room', None, None, 'Targets'), 'on_change_with_targets') unit_price = fields.Numeric('Unit Price', digits=(16, 4), required=True) @fields.depends('accommodation', 'departure_date', 'arrival_date') def on_change_with_unit_price(self): Booking = Pool().get('hotel.booking') booking = Booking(Transaction().context.get('active_id')) ctx = {} if booking.price_list: ctx['price_list'] = booking.price_list ctx['sale_date'] = self.arrival_date ctx['currency'] = booking.currency.id if booking.party: ctx['customer'] = booking.party.id if self.accommodation and self.departure_date and self.arrival_date: product = self.accommodation unit_price = product.template.list_price quantity = (self.departure_date - self.arrival_date).days if booking.price_list: with Transaction().set_context(ctx): unit_price = booking.price_list.compute( booking.party, product, unit_price, quantity, product.default_uom) unit_price = booking.currency.round(unit_price) return unit_price @fields.depends('arrival_date', 'departure_date', 'accommodation', 'overbooking') def on_change_with_targets(self, name=None): pool = Pool() RoomTemplate = pool.get('hotel.room-product.template') Room = pool.get('hotel.room') Folio = pool.get('hotel.folio') res = [] if not self.accommodation or not self.arrival_date or not self.departure_date: return res if self.overbooking: return [r.id for r in Room.search([])] room_templates = RoomTemplate.search([ ('template.accommodation_capacity', '>=', self.accommodation.accommodation_capacity) ]) rooms_ids = [t.room.id for t in room_templates] rooms_available_ids = Folio.get_available_rooms( self.arrival_date, self.departure_date, rooms_ids=rooms_ids ) return rooms_available_ids class SelectRooms(Wizard): 'Select Rooms' __name__ = 'hotel.booking.select_rooms' """ this is the wizard that allows the front desk employee to select rooms, based on the requirements listed by the customer. """ start = StateView( 'hotel.booking.select_rooms.ask', 'hotel.view_select_rooms_form', [ Button('Exit', 'end', 'tryton-cancel'), Button('Add and Continue', 'add_continue', 'tryton-forward'), Button('Add', 'add_rooms', 'tryton-ok'), ] ) add_rooms = StateTransition() add_continue = StateTransition() def transition_add_rooms(self): self._add_rooms() return 'end' def transition_add_continue(self): self._add_rooms() return 'start' def _add_rooms(self): pool = Pool() 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 and booking.is_person(): 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 BookingStatementLine(ModelSQL): 'Booking - Statement Line' __name__ = 'hotel.booking-statement.line' _table = 'hotel_booking_statement_line_rel' booking = fields.Many2One('hotel.booking', 'Booking', ondelete='CASCADE', select=True, required=True) statement_line = fields.Many2One('account.statement.line', 'Statement Line', ondelete='CASCADE', required=True) class 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), ('departure_date', '>', start_date), ('registration_state', 'in', ['check_in', 'check_out']), ]) def _get_default_room(r): res = { 'room': r.name, 'guest': None, 'num_guest': None, 'party': None, 'arrival': None, 'departure': None, 'booking': None, 'registration_card': None, 'amount': 0, 'registration_state': None, 'pending_total': 0, } return res rooms_map = {room.id: _get_default_room(room) for room in all_rooms} occupancy_rooms = 0 for fo in folios: rooms_map[fo.room.id].update({ 'guest': fo.main_guest.name, 'num_guest': len(fo.guests), 'party': fo.booking.party.name if fo.booking.party else '', 'arrival': fo.arrival_date, 'departure': fo.departure_date, 'registration_card': fo.registration_card, 'amount': fo.total_amount, 'booking': fo.booking.number, 'registration_state': fo.registration_state_string, 'pending_total': fo.pending_total, }) occupancy_rooms += 1 if all_rooms: occupancy_rate = (float(len(folios)) / len(all_rooms)) * 100 else: occupancy_rate = 0 report_context['records'] = rooms_map.values() report_context['occupancy_rate'] = occupancy_rate report_context['occupancy_rooms'] = occupancy_rooms report_context['company'] = Company(data['company']) report_context['date'] = data['date'] report_context['user'] = User(Transaction().user).rec_name return report_context class BookingStatusStart(ModelView): 'Booking Status Start' __name__ = 'hotel.print_booking_status.start' start_date = fields.Date('Start Date', required=True) end_date = fields.Date('End Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) grouped = fields.Boolean('Grouped') state = fields.Selection([ ('offer', 'Offer'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('not_show', 'Not Show'), ('finished', 'Finished'), ('', ''), ], 'State') @staticmethod def default_date(): Date_ = Pool().get('ir.date') return Date_.today() @staticmethod def default_company(): return Transaction().context.get('company') class BookingStatus(Wizard): 'Booking Status' __name__ = 'hotel.booking_status' start = StateView( 'hotel.print_booking_status.start', 'hotel.print_booking_status_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Open', 'print_', 'tryton-print', default=True), ]) print_ = StateReport('hotel.booking_status.report') def do_print_(self, action): company = self.start.company data = { 'start_date': self.start.start_date, 'end_date': self.start.end_date, 'company': company.id, 'state': self.start.state, 'grouped': self.start.grouped, } return action, data def transition_print_(self): return 'end' class BookingStatusReport(Report): __name__ = 'hotel.booking_status.report' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) pool = Pool() Company = pool.get('company.company') Folio = pool.get('hotel.folio') dom = [ ('arrival_date', '>=', data['start_date']), ('arrival_date', '<=', data['end_date']), ] if data['state']: dom.append( ('booking.state', '=', data['state']) ) records = Folio.search(dom, order=[('arrival_date', 'ASC')]) _records = {} for fol in records: bk = fol.booking if bk not in _records.keys(): _records[bk] = [] _records[bk].append(fol) report_context['grouped'] = data['grouped'] report_context['records'] = _records report_context['company'] = Company(data['company']) return report_context class InvoicePaymentForm(ModelView): 'Invoice Payment Form' __name__ = 'invoice.payment.form' payment_mode = fields.Many2One('account.voucher.paymode', 'Payment Mode', required=True) payment_amount = fields.Numeric('Payment amount', digits=(16, 2), required=True) party = fields.Many2One('party.party', 'Party', required=True) pay_date = fields.Date('Advance Date', required=True) reference = fields.Char('Reference') @classmethod def default_advance_date(cls): Date = Pool().get('ir.date') return Date.today() class UpdateHolderStart(ModelView): 'Update Holder Start' __name__ = 'hotel.update_holder.start' name = fields.Char('Name', required=True) sex = fields.Selection([ ('', ''), ('male', 'Male'), ('female', 'Female'), ], 'Sex') email = fields.Char('Email', required=True) mobile = fields.Char('Mobile') phone = fields.Char('Phone') birthday = fields.Date('Birthday') nationality = fields.Many2One('country.country', 'Nationality') origin_country = fields.Many2One('party.country_code', 'Origin Country') target_country = fields.Many2One('party.country_code', 'Target Country') country = fields.Many2One('party.country_code', 'Country') subdivision = fields.Many2One('party.department_code', 'Subdivision') city = fields.Many2One('party.city_code', 'City', domain=[ ('department', '=', Eval('subdivision')) ]) address = fields.Char('Address', required=True) type_document = fields.Selection(TYPE_DOCUMENT, 'Tipo de Documento', required=True) id_number = fields.Char('Id Number', required=True) visa_number = fields.Char('Visa Number') visa_date = fields.Date('Visa Date') notes = fields.Text('Notes') customer = fields.Many2One('party.party', 'Party') customer_id_number = fields.Char('Customer Id Number') customer_name = fields.Char('Customer Name') customer_country = fields.Many2One('party.country_code', 'Customer Country') customer_subdivision = fields.Many2One('party.department_code', 'Customer Subdivision') customer_city = fields.Many2One('party.city_code', 'Customer City') customer_address = fields.Char('Customer Address') customer_phone = fields.Char('Customer Phone') customer_email = fields.Char('Customer Email') customer_type_document = fields.Selection(TYPE_DOCUMENT, 'Customer Type Doc.') main_guest = fields.Boolean('Main Guest') vehicle_plate = fields.Char('Vehicle Plate') @fields.depends('id_number', 'name', 'sex', 'email', 'mobile', 'visa_number', 'visa_date', 'address', 'birthday', 'nationality') def on_change_id_number(self): if self.id_number: pool = Pool() Party = pool.get('party.party') Guest = pool.get('hotel.folio.guest') clause = [('id_number', '=', self.id_number)] parties = Party.search(clause) if not parties: parties = Guest.search(clause) if not parties: return address = '' country = None subdivision = None party = parties[0] if hasattr(party, 'addresses') and party.addresses: _address = party.addresses[0] address = _address.street country = _address.country_code and _address.country_code.id subdivision = _address.department_code and _address.department_code.id elif hasattr(party, 'address') and party.address: address = party.address self.name = party.name self.sex = party.sex self.email = party.email self.mobile = party.mobile self.visa_number = party.visa_number self.visa_date = party.visa_date self.birthday = party.birthday self.nationality = party.nationality and party.nationality.id self.country = country self.subdivision = subdivision self.address = address class UpdateHolder(Wizard): 'Update Holder' __name__ = 'hotel.update_holder' start = StateView( 'hotel.update_holder.start', 'hotel.update_holder_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Update', 'update', 'tryton-ok', default=True), ]) update = StateTransition() def default_start(self, fields): pool = Pool() Booking = pool.get('hotel.booking') Config = pool.get('hotel.configuration') config = Config.get_configuration() booking = Booking(Transaction().context['active_id']) party = booking.party nationality_id = None country_id = None if config.nationality: nationality_id = config.nationality.id if config.country: country_id = config.country.id res = { 'nationality': nationality_id, 'country': country_id, } if party and party.id > 0: if party.nationality: nationality_id = party.nationality.id address = party.addresses[0] if party.addresses else None res = { 'name': party.name.upper(), 'nationality': nationality_id, 'id_number': party.id_number, 'type_document': party.type_document, 'sex': party.sex, 'mobile': party.mobile, 'phone': party.phone, 'email': party.email, 'visa_number': party.visa_number, 'visa_date': party.visa_date, 'birthday': party.birthday, 'notes': party.notes, } if address: res['country'] = address.country_code.id if address.country_code else None res['city'] = address.city_code.id if address.city_code else None res['subdivision'] = address.department_code.id if address.department_code else None elif booking.contact: res['name'] = booking.contact.upper() return res def _set_cms(self, action, rec, email, mobile=None, phone=None): cms = [] # contact_mechanisms if mobile: cms.append({'type': 'mobile', 'value': mobile}) if email: cms.append({'type': 'email', 'value': email}) if phone: cms.append({'type': 'phone', 'value': phone}) if cms: rec['contact_mechanisms'] = [(action, cms)] return def transition_update(self): pool = Pool() Booking = pool.get('hotel.booking') Folio = pool.get('hotel.folio') Guest = pool.get('hotel.folio.guest') Party = pool.get('party.party') Address = pool.get('party.address') CM = pool.get('party.contact_mechanism') active_id = Transaction().context.get('active_id', False) edit = True booking = Booking(active_id) _party = self.start to_folio = {} if _party.vehicle_plate: to_folio['vehicle_plate'] = _party.vehicle_plate nationality_id = _party.nationality.id if _party.nationality else None rec = { 'name': _party.name.upper(), 'nationality': nationality_id, 'sex': _party.sex, 'birthday': _party.birthday, 'type_document': _party.type_document, 'id_number': _party.id_number, 'visa_number': _party.visa_number, 'visa_date': _party.visa_date, 'notes': _party.notes, } country_code = _party.country.id if _party.country else None city_code = _party.city.id if _party.city else None subdivision_code = _party.subdivision.id if _party.subdivision else None street = _party.address.upper() if _party.address else '' address = {} address['country_code'] = country_code address['city_code'] = city_code address['department_code'] = subdivision_code address['street'] = street party = None if not booking.party: edit = False parties = Party.search([ ('id_number', '=', _party.id_number), ]) if not parties: self._set_cms('create', rec, _party.email, _party.mobile, _party.phone) rec['addresses'] = [('create', [address])] # raise UserError('Este cliente ya existe!') else: party = parties[0] edit = True else: party = booking.party if party: if party.addresses: Address.write(list(party.addresses), address) else: Address.create([address]) cms_add = {} if _party.mobile: cms_add['mobile'] = _party.mobile cms_add['phone'] = _party.phone cms_add['email'] = _party.email if party.contact_mechanisms: for cm in party.contact_mechanisms: if cm.type == 'mobile' and _party.mobile: cm.value = cms_add.pop('mobile') elif cm.type == 'phone' and _party.phone: cm.value = cms_add.pop('phone') elif cm.type == 'email' and _party.email: cm.value = cms_add.pop('email') cm.save() if cms_add: for (key, value) in cms_add.items(): if not value: continue cm = CM(party=party.id, type=key, value=value) cm.save() else: self._set_cms('create', rec, _party.email, _party.mobile, _party.phone) rec_ = None if _party.customer_id_number and _party.customer_name and _party.customer_type_document: rec_ = { 'name': _party.customer_name, 'id_number': _party.customer_id_number, 'type_document': _party.customer_type_document, } address_cust = { 'country_code': None, 'city_code': None, 'street': '', } if _party.customer_country: address_cust['country_code'] = _party.customer_country.id if _party.customer_city: address_cust['city_code'] = _party.customer_city.id if _party.customer_address: address_cust['street'] = _party.customer_address.upper() rec_['addresses'] = [('create', [address_cust])] self._set_cms( 'create', rec_, _party.customer_email, phone=_party.customer_phone, ) party, = Party.create([rec_]) if edit: Party.write([party], rec) if not booking.party: booking.party = party.id else: party, = Party.create([rec]) Booking.write([booking], {'party': party.id}) booking.save() if _party.type_document != '31': for folio in booking.lines: if not folio.main_guest: folio.main_guest = party.id folio.save() _ = rec.pop('contact_mechanisms', None) _ = rec.pop('addresses', None) rec['address'] = address['street'] rec['country'] = country_code rec['nationality'] = nationality_id rec['folio'] = folio.id rec['main_guest'] = True rec['email'] = _party.email rec['mobile'] = _party.mobile Guest.create([rec]) break for folio in booking.lines: Folio.write([folio], to_folio) return 'end' class ManagerStart(ModelView): 'Manager Start' __name__ = 'hotel.print_manager.start' start_date = fields.Date('Start Date', required=True) end_date = fields.Date('End Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) @staticmethod def default_company(): return Transaction().context.get('company') class Manager(Wizard): 'Manager' __name__ = 'hotel.print_manager' start = StateView( 'hotel.print_manager.start', 'hotel.print_manager_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Open', 'print_', 'tryton-print', default=True), ]) print_ = StateReport('hotel.manager.report') def do_print_(self, action): company = self.start.company data = { 'start_date': self.start.start_date, 'end_date': self.start.end_date, 'company': company.id, } return action, data def transition_print_(self): return 'end' class ManagerReport(Report): __name__ = 'hotel.manager.report' @classmethod def get_location(self, party, field): for address in party.addresses: if hasattr(address, field): value = getattr(address, field + '_code') if value: return value @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) pool = Pool() Company = pool.get('company.company') Folio = pool.get('hotel.folio') Room = pool.get('hotel.room') User = pool.get('res.user') rooms = Room.search([]) total_rooms = len(rooms) delta_days = (data['end_date'] - data['start_date']).days + 1 folios = Folio.search([ ('arrival_date', '>=', data['start_date']), ('arrival_date', '<=', data['end_date']), ]) list_channels = [ ('ota', 'OTAS'), ('direct', 'DIRECTOS'), ('special_price', 'TARIFA ESPECIAL'), ('courtesy', 'CORTESIA'), ('house_use', 'USO DE CASA'), ] channels = {} rooms_occupied = [] for ch, desc in list_channels: channels[ch] = { 'name': desc, 'room_qty': [], 'room_rate': 0, 'num_adults': [], 'num_children': [], 'income': [], 'adr': 0, 'rev_par': 0, } local_guests = [] foreign_guests = [] total_income = [] rooms_saled = [] guests_by_country = {} guests_by_city = {} for folio in folios: if folio.booking.channel: type_ = 'ota' else: type_ = 'direct' channels[type_]['room_qty'].append(folio.nights_quantity) channels[type_]['num_adults'].append(folio.num_adults) channels[type_]['num_children'].append(folio.num_children) channels[type_]['income'].append(folio.total_amount) total_income.append(folio.total_amount) # parties = [guest.party for guest in folio.guests] for guest in folio.guests: if not guest or not guest.nationality: continue # print('guest ', guest, folio.booking.number) if guest.nationality.name == 'COLOMBIA': city = cls.get_location(folio.main_guest, 'city') local_guests.append(1) if not city: continue if city not in guests_by_city.keys(): guests_by_city[city] = { 'name': city.name, 'persons': [], 'nights': [], } guests_by_city[city]['persons'].append(1) guests_by_city[city]['nights'].append(folio.nights_quantity) else: country = cls.get_location(folio.main_guest, 'country') foreign_guests.append(1) if not country: continue if country not in guests_by_country.keys(): guests_by_country[country] = { 'name': country.name, 'persons': [], 'nights': [], } guests_by_country[country]['persons'].append(1) guests_by_country[country]['nights'].append(folio.nights_quantity) for k, v in channels.items(): room_qty = sum(v['room_qty']) income = sum(v['income']) v['room_qty'] = room_qty v['room_rate'] = room_qty / (total_rooms * delta_days) v['num_adults'] = sum(v['num_adults']) v['num_children'] = sum(v['num_children']) v['income'] = income if income > 0: v['adr'] = income / room_qty v['rev_par'] = income / delta_days rooms_occupied.append(room_qty) if k in ('direct', 'ota'): rooms_saled.append(room_qty) available_nights = total_rooms * delta_days beds_capacity = [] for room in rooms: beds_capacity.append(room.main_accommodation.accommodation_capacity or 0) available_beds = sum(beds_capacity) * delta_days average_price = sum(total_income) / sum(rooms_saled) report_context['records'] = channels.values() report_context['rooms_occupied'] = sum(rooms_occupied) report_context['rooms_saled'] = sum(rooms_saled) report_context['gross_occupancy'] = sum(rooms_occupied) / available_nights report_context['net_occupancy'] = sum(rooms_saled) / available_nights report_context['total_income'] = sum(total_income) report_context['local_guests'] = sum(local_guests) report_context['foreign_guests'] = sum(foreign_guests) report_context['available_nights'] = available_nights report_context['available_beds'] = available_beds report_context['average_price'] = average_price report_context['guests_by_country'] = guests_by_country.values() report_context['guests_by_city'] = guests_by_city.values() report_context['company'] = Company(data['company']) user_id = Transaction().context.get('user') report_context['user'] = User(user_id) return report_context class BookingChannelCommision(ModelSQL): 'Booking Channel Commision' __name__ = 'hotel.booking-channel.commission' _table = 'hotel_booking_channel_commission_rel' commission = fields.Many2One('hotel.channel.commission', 'Channel Commission', ondelete='CASCADE', required=True) booking = fields.Many2One('hotel.booking', 'Booking', ondelete='RESTRICT', required=True) class StatementPaymentForm(ModelView): 'Statement Payment Form' __name__ = 'hotel.payment_form.start' statement = fields.Many2One('account.statement', 'Statement', required=True, domain=['OR', [ ('create_uid.login', '=', Eval('user')), ('state', '=', 'draft') ], [ Eval('user') == 'admin', ('state', '=', 'draft'), ]]) amount_to_pay = fields.Numeric('Amount to Pay', required=True, digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']) currency_digits = fields.Integer('Currency Digits') kind = fields.Selection([ ('booking', 'Booking'), ('folio', 'Folio'), ], 'Kind', required=True) folio = fields.Many2One('hotel.folio', 'Folio', domain=[ ('id', 'in', Eval('folios')), ], states={ 'invisible': Eval('kind') == 'booking', 'required': Eval('kind') == 'folio', }, depends=['folios', 'kind'] ) voucher = fields.Char('Voucher', states={ 'required': Bool(Eval('require_voucher')), }, depends=['require_voucher']) party = fields.Many2One('party.party', 'Party', domain=[ ('id', 'in', Eval('parties')) ], required=True) user = fields.Many2One('res.user', 'User', states={'readonly': True}) require_voucher = fields.Boolean('Require Voucher', depends=['statement']) parties = fields.Many2Many('party.party', None, None, 'Parties') folios = fields.Many2Many('hotel.folio', None, None, 'Folios') @classmethod def default_require_voucher(cls): return False @classmethod def default_user(cls): user = Pool().get('res.user')(Transaction().user) return user.id @classmethod def default_kind(cls): model = Transaction().context['active_model'] if model == "hotel.folio": return "folio" else: return "booking" @fields.depends('statement', 'require_voucher') def on_change_with_require_voucher(self): if self.statement: return self.statement.journal.require_voucher return False class WizardStatementPayment(Wizard): 'Wizard Statement Payment' __name__ = 'hotel.payment_form' start = StateView( 'hotel.payment_form.start', 'hotel.statement_payment_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Pay', 'pay_', 'tryton-ok', default=True), ]) pay_ = StateTransition() def default_start(self, fields): pool = Pool() Booking = pool.get('hotel.booking') Folio = pool.get('hotel.folio') model = Transaction().context['active_model'] active_id = Transaction().context['active_id'] parties_ids = [] folios_ids = [] folio_id = None party_id = None pending_to_pay = 0 kind = 'booking' if model == "hotel.booking": booking = Booking(active_id) if not booking.party: raise UserError(gettext('hotel.msg_missing_party_holder')) if booking.responsible_payment in ('holder', 'holder_guest'): parties_ids.append(booking.party.id) if booking.responsible_payment in ('guest', 'holder_guest'): for folio in booking.lines: parties_ids.append(folio.main_guest.id) folios_ids.append(folio.id) pending_to_pay = booking.pending_to_pay else: kind = 'folio' folio = Folio(active_id) folios_ids.append(folio.id) parties_ids.append(folio.main_guest.id) if folio.booking.responsible_payment == 'guest': pending_to_pay = folio.pending_total elif folio.booking.responsible_payment == 'holder_guest': pending_to_pay = folio.pending_charges if folio.booking.party == folio.main_guest and \ folio.booking.responsible_payment == 'holder': pending_to_pay = folio.booking.pending_to_pay return { 'currency_digits': 2, 'party': party_id, 'folio': folio_id, 'parties': parties_ids, 'folios': folios_ids, 'amount_to_pay': pending_to_pay, 'kind': kind, } def transition_pay_(self): pool = Pool() Booking = pool.get('hotel.booking') Folio = pool.get('hotel.folio') StatementLine = pool.get('account.statement.line') active_id = Transaction().context.get('active_id', False) form = self.start target_model = 'booking' if form.kind == 'booking': record = Booking(active_id) description = record.number else: target_model = 'folio' record = Folio(form.folio.id) if record.booking.responsible_payment != 'holder': description = record.registration_card else: booking = record.booking description = booking.number target_model = 'booking' try: account = form.party.account_receivable.id except: raise UserError(gettext( 'hotel.party_without_account_receivable')) number = '' if form.voucher: number = form.voucher if form.amount_to_pay: line = StatementLine( statement=form.statement.id, date=date.today(), amount=form.amount_to_pay, party=form.party.id, account=account, number=number, description=description, voucher=self.start.voucher, source=str(record), ) line.save() line.create_move() # We keep to mark all concepts charges/folios as paid if # the customer to pay all pending values if form.kind == 'folio': if target_model == 'folio': folio = record if folio.booking.responsible_payment == 'guest': pending = folio.pending_total elif folio.booking.responsible_payment == 'holder_guest': pending = folio.pending_charges if form.amount_to_pay >= pending: for charge in record.charges: if charge.status == 'paid': continue charge.status = 'paid' charge.save() folio.payment_status = 'paid' if target_model == 'booking': if form.amount_to_pay >= record.pending_to_pay: for folio in record.lines: for charge in folio.charges: if charge.status == 'paid': continue charge.status = 'paid' charge.save() folio.payment_status = 'paid' folio.save() record.save() return 'end' class BillBookingStart(ModelView): 'Bill Booking Form' __name__ = 'hotel.bill_booking.start' kind = fields.Selection([ ('all', 'All'), ('only_accommodation', 'Only Accommodation'), ('only_products', 'Only Products'), ], 'Kind', required=True) party = fields.Many2One('party.party', 'Bill To', domain=[ ('id', 'in', Eval('parties')), ]) parties = fields.Many2Many('party.party', None, None, 'Parties') class BillBooking(Wizard): 'Bill Booking' __name__ = 'hotel.bill_booking' start = StateView( 'hotel.bill_booking.start', 'hotel.bill_booking_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Invoice', 'bill', 'tryton-ok', default=True), ]) bill = StateTransition() def default_start(self, fields): pool = Pool() Booking = pool.get('hotel.booking') Folio = pool.get('hotel.folio') active_id = Transaction().context.get('active_id', False) active_model = Transaction().context.get('active_model') parties_ids = [] if active_model == 'hotel.booking': bk = Booking(active_id) if bk.party: parties_ids = [bk.party.id] for fo in bk.lines: if fo.main_guest: parties_ids.append(fo.main_guest.id) else: folio = Folio(active_id) parties_ids = [folio.id] return { 'parties': parties_ids, } def transition_bill(self): pool = Pool() Booking = pool.get('hotel.booking') Folio = pool.get('hotel.folio') active_id = Transaction().context.get('active_id', False) active_model = Transaction().context.get('active_model') folios = [] if active_model == 'hotel.booking': bk = Booking(active_id) folios = bk.lines else: folio = Folio(active_id) folios = [folio] bk = folio.booking Booking.create_invoice(folios, kind=self.start.kind, party=self.start.party) Booking.concile_charges([bk]) Booking.check_finished([bk]) return 'end' # class AddOccupancy(Wizard): # pass