# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from decimal import Decimal from datetime import timedelta, datetime, date from collections import OrderedDict import calendar from trytond.model import ModelView, Workflow, ModelSQL, fields from trytond.pyson import Eval, Bool from trytond.pool import Pool from trytond.report import Report from trytond.wizard import ( Wizard, StateView, StateAction, Button, StateTransition, StateReport ) from trytond.transaction import Transaction from trytond.exceptions import UserError from trytond.i18n import gettext from .constants import ( REGISTRATION_STATE, COMPLEMENTARY, INVOICE_STATES, TYPE_DOCUMENT, PLAN, ) from .room import ROOM_STATUS from .siat import send_siat STATES_CHECKIN = { 'readonly': Eval('registration_state').in_( ['check_in', 'no_show', 'cancelled'] ), 'required': Eval('registration_state') == 'check_in', } STATES_OP = { 'readonly': Eval('state').in_(['check_out', 'done', 'cancelled']) } STATES_MNT = {'readonly': Eval('state') != 'draft'} _ZERO = Decimal('0') ROUND_TWO = Decimal('0.01') class Folio(ModelSQL, ModelView): 'Folio' __name__ = 'hotel.folio' STATES = { 'required': Eval('registration_state') == 'check_in', 'readonly': Eval('registration_state') == 'check_in', } _CHECKOUT = { 'readonly': Eval('registration_state').in_( ['check_out', 'no_show', 'cancelled'] ), 'required': Eval('registration_state') == 'check_in', } booking = fields.Many2One('hotel.booking', 'Booking', ondelete='CASCADE') reference = fields.Char('Reference', states={ 'readonly': Eval('registration_state').in_(['check_in', 'check_out']) }) registration_card = fields.Char('Registration Card', readonly=True, help="Unique sequence for card guest registration.") room = fields.Many2One('hotel.room', 'Room', states=_CHECKOUT) arrival_date = fields.Date('Arrival Date', required=True, states={ 'readonly': Eval('registration_state').in_(['check_out', 'check_in']) }) departure_date = fields.Date('Departure Date', required=True, states={ 'readonly': Eval('registration_state').in_(['check_out', 'check_in']) }) product = fields.Many2One('product.product', 'Product', domain=[ ('template.type', '=', 'service'), ('template.kind', '=', 'accommodation'), ], required=True, states={'readonly': True}) unit_price = fields.Numeric('Unit Price', digits=(16, 2), states={ 'required': Bool(Eval('product')), 'readonly': Eval('registration_state') == 'check_out', }) unit_price_w_tax = fields.Function(fields.Numeric('Unit Price w Tax', digits=(16, 2)), 'get_unit_price_w_tax') uom = fields.Many2One('product.uom', 'UOM', readonly=True) main_guest = fields.Many2One('party.party', 'Main Guest', states={'readonly': True}) contact = fields.Char('Contact', states={ 'readonly': Eval('registration_state').in_(['check_in', 'check_out'])} ) party_holder = fields.Function(fields.Many2One('party.party', 'Party Holder'), 'get_party_holder') num_children = fields.Function(fields.Integer('No. Children'), 'get_num_children') nights_quantity = fields.Integer('Nights', states={'readonly': True}, depends=['arrival_date', 'departure_date']) host_quantity = fields.Integer('Host', states=STATES_CHECKIN) estimated_arrival_time = fields.Time('Estimated Arrival Time', states={ 'readonly': Eval('registration_state').in_( ['check_out', 'no_show', 'cancelled'] )}) unit_digits = fields.Function(fields.Integer('Unit Digits'), 'get_unit_digits') notes = fields.Text('Notes') total_amount = fields.Function(fields.Numeric('Total Amount', digits=(16, 2)), 'get_total_amount') commission_amount = fields.Numeric('Commission Amount', digits=(16, 2), depends=['nights_quantity', 'unit_price'], states={'readonly': True} ) guests = fields.One2Many('hotel.folio.guest', 'folio', 'Guests', states={'readonly': Eval('registration_state').in_(['check_out'])}) storage = fields.Many2One('stock.location', 'Storage', domain=[('type', '=', 'storage')], states={ 'readonly': Eval('registration_state') != 'check_in', }, depends=['registration_state']) registration_state = fields.Selection(REGISTRATION_STATE, 'Registration State', readonly=True) registration_state_string = registration_state.translated( 'registration_state') charges = fields.One2Many('hotel.folio.charge', 'folio', 'Charges', states={ 'readonly': ~Eval('registration_state').in_( ['pending', 'check_in']), }) party = fields.Many2One('party.party', 'Party to Bill', states={ 'readonly': Eval('registration_state').in_(['check_out']), }) invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line') to_invoice = fields.Boolean('To Invoice', states={ 'invisible': Bool(Eval('invoice_line')), }, depends=['invoice_line'], help='Mark this checkbox if you want to invoice this item') invoice = fields.Function(fields.Many2One('account.invoice', 'Invoice', depends=['invoice_line']), 'get_invoice') invoice_state = fields.Function(fields.Selection( INVOICE_STATES, 'Invoice State'), 'get_invoice_state') invoice_state_string = invoice_state.translated('invoice_state') type_complementary = fields.Selection(COMPLEMENTARY, 'Type Complementary', states={ 'invisible': ~Bool(Eval('complementary')), 'required': Bool(Eval('complementary')), }) breakfast_included = fields.Boolean('Breakfast Included', states={ 'readonly': Eval('registration_state') != 'check_in', }) room_amount = fields.Function(fields.Numeric('Room Amount', digits=(16, 2), depends=['nights_quantity', 'unit_price'] ), 'on_change_with_room_amount') channel = fields.Function(fields.Many2One('hotel.channel', 'Channel'), 'get_channel') num_children = fields.Function(fields.Integer('Num. Children'), 'get_num_guests') num_adults = fields.Function(fields.Integer('Num. Adults'), 'get_num_guests') stock_moves = fields.Function(fields.One2Many('stock.move', 'origin', 'Moves'), 'get_stock_moves') vehicle_plate = fields.Char('Vehicle Plate') room_status = fields.Function(fields.Selection(ROOM_STATUS, 'Room Status'), 'get_room_status') payment_status = fields.Selection([ (None, ''), ('pending', 'Pending'), ('paid', 'Paid'), ], 'Acco. Payment Status', readonly=True) payments = fields.One2Many('account.statement.line', 'source', 'Payments', readonly=True) occupancy = fields.One2Many('hotel.folio.occupancy', 'folio', 'Occupancy', readonly=True) pending_accommodation = 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') pending_total = fields.Function(fields.Numeric('Pending to Pay', digits=(16, 2)), 'get_pending_to_pay') total_advances = fields.Function(fields.Numeric('Total Advances', digits=(16, 2)), 'get_total_advances') pax = fields.Integer('PAX', states={ 'readonly': Eval('registration_state').in_( ['check_out', 'no_show', 'cancelled'] )}, help="Number of persons in house setted manually.") invoices = fields.Function(fields.Many2Many('account.invoice', None, None, 'Invoices'), 'get_invoices') charges_blocked = fields.Boolean('Charges Blocked') taxes_exception = fields.Boolean('Taxes Exception') plan = fields.Function(fields.Selection(PLAN, 'Commercial Plan'), 'get_plan') group = fields.Function(fields.Boolean('Group'), 'get_group') @classmethod def __setup__(cls): super(Folio, cls).__setup__() cls._buttons.update({ 'check_in': { 'invisible': Eval('registration_state').in_( ['check_in', 'check_out'] ), }, 'check_out': { 'invisible': Eval('registration_state') != 'check_in', }, 'bill': { 'invisible': Eval('registration_state') != 'check_out', }, 'load_accommodation': { 'invisible': ~Eval('registration_state'), }, 'do_payment': { 'invisible': Eval('registration_state').in_([ 'no_show', 'cancelled', 'pending' ]), } }) @staticmethod def default_to_invoice(): return True @staticmethod def default_payment_status(): return 'pending' @staticmethod def default_registration_state(): return 'pending' @staticmethod def default_pax(): return 1 @classmethod def delete(cls, records): for folio in records: for occ in folio.occupancy: if occ.charge: raise UserError(gettext('hotel.msg_folio_with_charges')) super().delete(records) @classmethod @ModelView.button_action('hotel.wizard_statement_payment_form') def do_payment(cls, records): pass def get_group(self, name=None): if self.booking: return self.booking.group def get_total_advances(self, name=None): res = [] for pay in self.payments: res.append(pay.amount) return sum(res) def pending_occupancy(self, name=None): res = [] for occ in self.occupancy: if occ.charge: continue res.append({ 'name': self.product.template.name, 'date': occ.occupancy_date, 'quantity': 1, 'unit_price': occ.unit_price_w_tax, 'amount': occ.unit_price_w_tax, }) return res def get_plan(self, name=None): if self.booking: return self.booking.plan def _get_paid(self, kind): res = [] for charge in self.charges: if charge.status == 'paid' and charge.kind == kind: res.append(charge.taxed_amount or 0) return sum(res) def get_pending_to_pay(self, name=None): res = 0 if name == 'pending_accommodation': res = self.get_total_accommodation() charge_paid = self._get_paid('accommodation') res = res - charge_paid elif name == 'pending_charges': res = self.get_total_products() charge_paid = self._get_paid('product') res = res - charge_paid elif name == 'pending_total': res = self.total_amount - self.total_advances res = round(res, 2) return res def get_num_guests(self, name): res = [] for guest in self.guests: if name == 'num_children' and guest.type_guest == 'child': res.append(1) elif name == 'num_adults' and guest.type_guest == 'adult': res.append(1) return sum(res) def get_party_holder(self, name=None): if self.booking.party: return self.booking.party.id def get_rec_name(self, name=None): if self.room: name = f'{self.room.code}:' if self.main_guest: name = name + self.main_guest.name return name @classmethod def search_rec_name(cls, name, clause): _, operator, value = clause if operator.startswith('!') or operator.startswith('not '): bool_op = 'AND' else: bool_op = 'OR' domain = [ bool_op, ('main_guest', operator, value), ('registration_card', operator, value), ('product.name', operator, value), ('party.name', operator, value), ('booking.party.name', operator, value), ('reference', operator, value), ] return domain def get_invoices(self, name=None): res = [] for charge in self.charges: if charge.invoice_line: res.append(charge.invoice_line.invoice.id) return list(set(res)) @classmethod def create(cls, values): folios = super(Folio, cls).create(values) for folio in folios: folio.update_nights(folio.arrival_date, folio.departure_date) folio.update_commission() folio.save() @classmethod def get_current_folios(cls): res = cls.search_read([ ('registration_state', '=', 'check_in'), ('arrival_date', '<=', date.today()), ('departure_date', '>=', date.today()), ('charges_blocked', '=', False), ], fields_names=[ 'booking.number', 'main_guest.name', 'main_guest.id_number', 'main_guest.phone', 'main_guest.account_receivable', 'main_guest.mobile', 'main_guest.account_payable', 'main_guest.address', 'main_guest.addresses', 'room.code', 'room.name', 'product.template.name', 'registration_state', 'arrival_date', 'departure_date', ]) return res @classmethod @ModelView.button def check_in(cls, records): for record in records: record.validate_check_in() if record.booking.state == 'offer': raise UserError(gettext('hotel.msg_missing_confirm_booking')) if record.main_guest is None: raise UserError(gettext('hotel.msg_missing_main_guest')) if record.room is None: raise UserError(gettext('hotel.msg_missing_select_room')) record.set_registration_number() record.check_room() record.add_occupancy('check_in') cls.update_room(record, 'check_in') cls.write(records, {'registration_state': 'check_in'}) @classmethod @ModelView.button def load_accommodation(cls, records): for folio in records: folio.add_charge_occupancy(ctx='allow') def check_room(self): if self.room.state != 'clean': raise UserError( gettext('hotel.msg_room_no_clean', room=self.room.name) ) def get_room_status(self, name): if self.room: return self.room.state def validate_check_in(self): if self.arrival_date > date.today(): raise UserError(gettext('hotel.msg_cannot_check_in_future')) def try_reconcile(self): # Try mark as paid all charges of the folio if there is # not amount pending to pay if self.booking.pending_to_pay == 0: for charge in self.charges: charge.status = 'paid' charge.save() def validate_check_out(self): bk = self.booking for occ in self.occupancy: if not occ.charge: raise UserError( gettext('hotel.msg_the_accommodation_not_charged')) def _check_accommodation(folio): if folio.booking.complementary: return for charge in folio.charges: if charge.status != 'paid': raise UserError( gettext('hotel.msg_accommodation_not_paid')) def _check_charges(folio): complementary = folio.booking.complementary for charge in folio.charges: if complementary and charge.product.template.kind == 'accommodation': return if charge.status != 'paid': raise UserError(gettext('hotel.msg_charges_not_paid')) if bk.responsible_payment == 'guest': _check_charges(self) _check_accommodation(self) elif bk.responsible_payment == 'holder': if bk.payment_term and bk.payment_term.payment_type == 2: return if self.main_guest == bk.party: for folio in bk.lines: if bk.pending_to_pay == 0: for charge in folio.charges: charge.status = 'paid' charge.save() continue _check_charges(folio) _check_accommodation(folio) else: _check_charges(self) _check_accommodation(self) else: _check_charges(self) self.sync_send_siat() def sync_send_siat(self): pool = Pool() config = pool.get('hotel.configuration').get_configuration() company = self.booking.company try: res = send_siat(company, self.main_guest, self, config.token_siat) if res: now = datetime.now() for guest in self.guests: if not guest.siat_send_date: guest.siat_send_date = now guest.siat_id = res['code'] guest.save() except Exception as error: print("Error en plataforma SIAT...!", error) @classmethod @ModelView.button def check_out(cls, records): for record in records: record.try_reconcile() record.validate_check_out() cls.write([record], {'registration_state': 'check_out'}) cls.update_room(record, 'check_out') @classmethod @ModelView.button_action('hotel.wizard_bill_booking') def bill(cls, records): pass @classmethod @ModelView.button_action('hotel.wizard_booking_advance_voucher') def pay(cls, records): pass @classmethod def update_room(cls, folio, status): pool = Pool() Configuration = pool.get('hotel.configuration') config = Configuration.get_configuration() cleaning_type_id = None room = folio.room if status in 'check_in' and config.cleaning_occupied: cleaning_type_id = config.cleaning_occupied.id room.last_check_in = datetime.now() room.last_check_out = None room.check_in_today = datetime.now().time() elif status in 'check_out' and config.cleaning_check_out: cleaning_type_id = config.cleaning_check_out.id room.last_check_out = datetime.now() room.last_check_in = None room.check_out_today = datetime.now().time() room.state = 'dirty' if cleaning_type_id: room.cleaning_type = cleaning_type_id room.save() def get_invoice_state(self, name=None): if self.invoice_line: return self.invoice_line.invoice.state def get_channel(self, name=None): if self.booking and self.booking.channel: return self.booking.channel.id def set_registration_number(self): """ Fill the number field for registration card with sequence """ pool = Pool() Config = pool.get('hotel.configuration') config = Config.get_configuration() if self.registration_card: return if not config.registration_card_sequence: raise UserError(gettext('hotel.msg_missing_sequence_registration')) number = config.registration_card_sequence.get() self.registration_card = number self.save() def get_stock_moves(self, name=None): moves = [] for charge in self.charges: if charge.move: moves.append(charge.move.id) return moves def get_unit_price_w_tax(self, name=None): Tax = Pool().get('account.tax') res = self.unit_price or 0 if self.unit_price and not self.taxes_exception: values = Tax.compute( self.product.template.customer_taxes_used, self.unit_price, 1) if values: value = values[0] res = value['base'] + value['amount'] return res @fields.depends('unit_price', 'nights_quantity') def on_change_with_room_amount(self, name=None): res = 0 if self.unit_price and self.nights_quantity: res = self.unit_price * self.nights_quantity return Decimal(round(res, Folio.total_amount.digits[1])) @staticmethod def default_main_guest(): party = Transaction().context.get('party') return party @staticmethod def default_host_quantity(): return 1 @staticmethod def default_accommodation(): Configuration = Pool().get('hotel.configuration') configuration = Configuration.get_configuration() if configuration.default_accommodation: return configuration.default_accommodation.id @staticmethod def default_breakfast_included(): return True @classmethod def validate(cls, lines): super(Folio, cls).validate(lines) for line in lines: # line.check_method() pass def get_invoice(self, name=None): if self.invoice_line and self.invoice_line.invoice: return self.invoice_line.invoice.id def get_room_info(self): description = ' \n'.join([ self.product.rec_name, 'Huesped Principal: ' + self.main_guest.name if self.main_guest else '', 'Habitacion: ' + self.room.name, 'Llegada: ' + str(self.arrival_date), 'Salida: ' + str(self.departure_date), ]) return description @fields.depends('product', 'unit_price', 'uom', 'booking', 'nights_quantity', 'commission_amount', 'occupancy') def on_change_product(self): Product = Pool().get('product.product') unit_price = None if self.product and self.booking: self.uom = self.product.default_uom.id with Transaction().set_context( self.booking.get_context_price(self.product)): unit_price = Product.get_sale_price([self.product], self.nights_quantity or 0)[self.product.id] # self.taxes = [tax.id for tax in self.product.customer_taxes_used] else: unit_price = self.product.list_price if unit_price and self.booking: unit_price = self.booking.currency.round(self.unit_price) self.update_occupancy(price=unit_price) self.update_commission() def update_commission(self): if self.nights_quantity and self.unit_price and self.booking: if not self.booking.channel: return amount = self.on_change_with_room_amount() self.commission_amount = self.booking.channel.compute(amount) @fields.depends('arrival_date', 'departure_date', 'unit_price', 'booking') def on_change_arrival_date(self): if self.arrival_date and self.departure_date: self.update_nights(self.arrival_date, self.departure_date) self.update_commission() @fields.depends('arrival_date', 'departure_date', 'unit_price', 'booking') def on_change_departure_date(self): if self.arrival_date and self.departure_date: self.update_nights(self.arrival_date, self.departure_date) self.update_commission() def check_method(self): """ Check the methods. """ Date = Pool().get('ir.date') if self.registration_state in (['check_in', 'check_out']): raise UserError(gettext('hotel.msg_reservation_checkin')) if self.arrival_date < Date.today(): raise UserError(gettext('hotel.msg_invalid_arrival_date')) if self.arrival_date >= self.departure_date: raise UserError(gettext('hotel.msg_invalid_date')) def get_state(self, name): if self.booking: return self.booking.state def get_host_quantity(self, name): res = 0 if self.num_adult: res += self.num_adult if self.num_children: res += self.num_children return res @classmethod def get_available_rooms(cls, start_date, end_date, rooms_ids=[], oper=None): """ Look for available rooms. given the date interval, return a list of room ids. A room is available if it has no operation that overlaps with the given date interval. the optional 'rooms' list is a list of room instance, is an additional filter, specifying the ids of the desirable rooms. the optional 'oper' is an operation object that has to be filtered out of the test. it is useful for validating an already existing operation. """ Maintenance = Pool().get('hotel.maintenance') if start_date >= end_date: raise UserError(gettext('hotel.msg_invalid_date_range')) # define the domain of the operations that find a # room to be available dom = ['AND', ['OR', [ ('arrival_date', '>=', start_date), ('arrival_date', '<', end_date), ], [ ('departure_date', '<=', end_date), ('departure_date', '>', start_date), ], [ ('arrival_date', '<=', start_date), ('departure_date', '>=', end_date), ], ]] ## If oper was specified, do not compare the operations with it if oper is not None: dom.append(('id', '!=', oper.id)) if rooms_ids: dom.append(('room', 'in', rooms_ids)) folios = cls.search(dom) rooms_not_available_ids = [folio.room.id for folio in folios] dom = ['AND', ['OR', [ ('start_date', '>=', start_date), ('start_date', '<=', end_date), ], [ ('end_date', '<=', end_date), ('end_date', '>=', start_date), ], [ ('start_date', '<=', start_date), ('end_date', '>=', end_date), ]], ('state', 'in', ('in_progress', 'finished')), ] maintenance = Maintenance.search(dom) rooms_not_available_ids.extend(mnt.room.id for mnt in maintenance) rooms_available_ids = set(rooms_ids) - set(rooms_not_available_ids) return list(rooms_available_ids) def update_nights(self, arrival_date, departure_date): self.nights_quantity = (departure_date - arrival_date).days def add_charge_occupancy(self, ctx=None): pool = Pool() Charge = pool.get('hotel.folio.charge') Date = pool.get('ir.date') # Add transaction context for hotel price list for current day unit_price = Decimal(round(self.unit_price, 2)) space = self.booking.space today = Date.today() for occ in self.occupancy: if occ.charge: continue if not ctx and occ.occupancy_date > today: continue analytic_account = space.analytic_account if space else None description = f"{self.room.name} | {self.registration_card}" rec = { 'folio': self.id, 'product': self.product.id, 'unit_price': unit_price, 'date_service': occ.occupancy_date, 'quantity': 1, 'status': 'pending', 'state': '', 'analytic_account': analytic_account, 'kind': 'accommodation', 'description': description, 'taxes': [], } if not self.taxes_exception: rec['taxes'] = [('add', [ tax.id for tax in self.product.customer_taxes_used ]) ] charge, = Charge.create([rec]) occ.charge = charge.id occ.save() def add_occupancy(self, next=None): FolioOccupancy = Pool().get('hotel.folio.occupancy') if self.occupancy: return res = [] delta = (self.departure_date - self.arrival_date).days current = [oc.occupancy_date for oc in self.occupancy] unit_price = round(self.unit_price, 2) for day in range(delta): _date = self.arrival_date + timedelta(days=day) if _date in current: continue res.append({ 'folio': self.id, 'occupancy_date': _date, 'unit_price': unit_price, 'state': 'draft', }) FolioOccupancy.create(res) @classmethod def get_folios(cls, args): pool = Pool() Maintenance = pool.get('hotel.maintenance') Room = pool.get('hotel.room') start = datetime.strptime(args['start_date'], "%Y-%m-%d") start_date = (start + timedelta(days=-4)).date() end_date = (start + timedelta(days=30)).date() dom = ['OR', [ ('arrival_date', '>=', start_date), ('arrival_date', '<=', end_date) ], [ ('departure_date', '>=', start_date), ('departure_date', '<=', end_date) ]] fields_names = [ "room.name", "arrival_date", "departure_date", "main_guest.name", "product.rec_name", "unit_price_w_tax", "pending_total", "plan", "guests", "charges", "booking.contact", "booking.party.name", "booking.number", "booking.media", "booking.channel.rec_name", "booking.channel.code", "registration_state", "registration_card", "nights_quantity", "notes", "vehicle_plate", ] rooms = Room.search_read([], fields_names=['code', 'name']) folios = cls.search_read(dom, fields_names=fields_names) occ_rate = cls.get_occupation_rate(start_date, end_date) dom_maint = ['OR', [ ('start_date', '>=', start_date), ('start_date', '<=', end_date), ('state', 'in', ('in_progress', 'finished', 'confirmed')), ('start_date', '!=', None), ('end_date', '!=', None), ], [ ('end_date', '>=', start_date), ('end_date', '<=', end_date), ('state', 'in', ('in_progress', 'finished', 'confirmed')), ('start_date', '!=', None), ('end_date', '!=', None), ], ] fields_mnt = [ 'start_date', 'end_date', 'room', 'room.name', 'issue', 'create_uid.name' ] mnts = Maintenance.search_read(dom_maint, fields_names=fields_mnt) res = { 'rooms': rooms, 'folios': folios, 'mnts': mnts, 'occ_rate': occ_rate } return res def get_total_accommodation(self): res = [] if self.booking.channel and \ self.booking.channel_payment_method == 'ota_collect': return 0 if self.registration_state in ['check_in', 'check_out']: for charge in self.charges: if charge.kind == 'accommodation': taxed_amount = charge.taxed_amount or 0 res.append(taxed_amount) for occ in self.occupancy: if not occ.charge: unit_price_w_tax = occ.unit_price_w_tax or 0 res.append(unit_price_w_tax) else: if self.nights_quantity and self.unit_price_w_tax: res = [self.nights_quantity * self.unit_price_w_tax] res = round(sum(res), 2) return res @classmethod def get_occupation_rate(cls, start_date, end_date): Room = Pool().get('hotel.room') alldays = OrderedDict() rooms = Room.search_read([], fields_names=['code', 'name']) n_rooms = len(rooms) delta = (end_date - start_date).days _date = start_date for nd in range(delta): alldays[_date] = { 'occ_rate': 0 } _date = _date + timedelta(days=1) dom = [ ['OR', [ ('arrival_date', '<=', start_date), ('departure_date', '>=', end_date), ], [ ('arrival_date', '>=', start_date), ('departure_date', '<=', end_date), ], [ ('arrival_date', '<=', end_date), ('departure_date', '>=', end_date), ], [ ('arrival_date', '<=', start_date), ('departure_date', '>=', end_date), ]], ('booking.state', 'not in', ['no_show', 'cancelled']) ] nfields = ['arrival_date', 'departure_date', 'nights_quantity'] folios = cls.search_read(dom, fields_names=nfields) bag_data = [] extend = bag_data.extend for folio in folios: extend([folio['arrival_date'] + timedelta(days=n) for n in range(folio['nights_quantity']) ]) for _day, dvalue in alldays.items(): dvalue['occ_rate'] = int(bag_data.count(_day) * 100 / n_rooms) return alldays @classmethod def set_offset_commission_move(cls, folios_lines, bk): """ Create Offset Move (Asiento de cruce) in order to cross the advance anticipated to the channel (supplier) against the account receivable in the sale invoice """ pool = Pool() Move = pool.get('account.move') Period = pool.get('account.period') MoveLine = pool.get('account.move.line') Config = pool.get('hotel.configuration') config = Config.get_configuration() period_id = Period.find(bk.company.id, date=date.today()) description = bk.number if bk.ota_booking_code: description += ' | ' + bk.ota_booking_code move, = Move.create([{ 'journal': config.offset_journal.id, 'period': period_id, 'date': date.today(), # 'origin': str(self), 'state': 'draft', 'description': description, }]) lines_ids = [] party_channel = bk.channel.agent.party party = bk.party for folio, inv_line in folios_lines: if not folio.commission_amount: return move_line, = MoveLine.create([{ 'description': folio.registration_card, 'account': party.account_receivable_used, 'party': party.id, 'debit': 0, 'credit': folio.commission_amount, 'move': move.id, }]) lines_ids.append(move_line.id) move_line, = MoveLine.create([{ 'description': folio.registration_card, 'account': party_channel.account_payable_used, 'party': party.id, 'debit': folio.commission_amount, 'credit': 0, 'move': move.id, }]) inv_line.offset_move_line = move_line.id inv_line.save() Move.post([move]) return lines_ids def get_total_products(self): res = [] for charge in self.charges: if charge.kind == 'product': res.append(charge.taxed_amount) return sum(res) def get_total_amount(self, name): """ The total amount of folio based on room flat price. TODO: If room fee is applied should be used for total price calculation instead of flat price. Fee is linked to channel management. """ res = [] res.append(self.get_total_accommodation()) res.append(self.get_total_products()) res = round(sum(res), Folio.total_amount.digits[1]) return res @fields.depends('nights_quantity', 'unit_price', 'booking') def on_change_with_commission_amount(self): """ Compute of commission amount for channel based on booking """ res = Decimal(0) if self.nights_quantity and self.unit_price and self.booking and \ self.booking.channel: amount = self.on_change_with_room_amount() res = self.booking.channel.compute(amount) return res def _create_occ(self, occ_date, unit_price): return { 'occupancy_date': occ_date, 'folio': self.id, 'unit_price': unit_price, 'state': 'draft', } @classmethod def write(cls, records, *args): super(Folio, cls).write(records, *args) for values in args: if not isinstance(values, dict): continue arr_date = values.get('arrival_date', None) dep_date = values.get('departure_date', None) unit_price = values.get('unit_price', None) if isinstance(arr_date, str): arr_date = datetime.strptime(arr_date, '%Y-%m-%d').date() if isinstance(dep_date, str): dep_date = datetime.strptime(dep_date, '%Y-%m-%d').date() if arr_date or dep_date or unit_price: for folio in records: folio.update_occupancy( arr_date=arr_date, dep_date=dep_date, price=unit_price) def update_occupancy(self, arr_date=None, dep_date=None, price=None): Occupancy = Pool().get('hotel.folio.occupancy') to_delete = [] to_create = [] price = price or self.unit_price arr_date = arr_date or self.arrival_date dep_date = dep_date or self.departure_date if dep_date or arr_date: self.update_nights(arr_date, dep_date) delta = (dep_date - arr_date).days current_dates = {occ.occupancy_date: occ for occ in self.occupancy} new_dates = [] for day in range(delta): new_dates.append(arr_date + timedelta(day)) for new_date in new_dates: if new_date not in current_dates.keys(): to_create.append(self._create_occ(new_date, price)) for curr_date, occ in current_dates.items(): if curr_date not in new_dates: if not occ.charge: to_delete.append(occ) continue raise UserError(gettext( 'hotel.msg_can_not_change_departure')) if to_create: self.occupancy Occupancy.create(to_create) if to_delete: Occupancy.delete(to_delete) if price: self.unit_price = price for occ in self.occupancy: occ.unit_price = price occ.save() if occ.charge: occ.charge.unit_price = price occ.charge.save() @classmethod def _get_check_rooms(cls, kind): # Dash Report # kind : arrival or departure field_target = kind + '_date' folios = cls.search_read([ (field_target, '=', date.today()) ], fields_names=['id']) return len(folios) @classmethod def report_check_in_today(cls, args): # Dash Report value = cls._get_check_rooms('arrival') today = date.today() res = { 'value': value, 'header_meta': str(today), } return res @classmethod def report_check_out_today(cls, args): # Dash Report value = cls._get_check_rooms('departure') today = date.today() res = { 'value': value, 'header_meta': str(today), } return res @classmethod def _get_occupation(cls, today): dom = [ ('arrival_date', '<=', today), ('departure_date', '>', today), ] folios = cls.search_read(dom, fields_names=['id', 'guests']) return folios @classmethod def report_occupation_by_room(cls, args): # Dash Report today = date.today() folios = cls._get_occupation(today) value = len(folios) res = { 'value': value, 'header_meta': str(today), } return res @classmethod def report_number_guests(cls, args): # Dash Report today = date.today() folios = cls._get_occupation(today) value = 0 for folio in folios: value += len(folio['guests']) res = { 'value': value, 'header_meta': str(today), } return res @classmethod def _get_range_dates(cls, folio, mode=None): start_date = folio['arrival_date'] res = [] _append = res.append for dt in range(folio['nights_quantity']): _date = start_date + timedelta(days=dt) if mode == 'string': _date = str(_date) _append(_date) return set(res) @classmethod def report_month_occupancy_rate(cls, args): pool = Pool() Room = pool.get('hotel.room') Folio = pool.get('hotel.folio') today = date.today() first, last = calendar.monthrange(today.year, today.month) labels = [] alldays = OrderedDict() dates_of_month = [] today = date.today() for nd in range(last): _day = str(date(today.year, today.month, nd + 1)) labels.append(str(nd + 1)) dates_of_month.append(_day) alldays[_day] = [] date_start = date(today.year, today.month, 1) date_end = date(today.year, today.month, last) dom = [ ['OR', [ ('arrival_date', '<=', date_start), ('departure_date', '>=', date_start), ], [ ('arrival_date', '>=', date_start), ('departure_date', '<=', date_end), ], [ ('arrival_date', '<=', date_end), ('departure_date', '>=', date_end), ], [ ('arrival_date', '<=', date_start), ('departure_date', '>=', date_end), ]], ('booking.state', 'not in', ['no_show', 'cancelled']) ] folios = Folio.search_read(dom, fields_names=[ 'arrival_date', 'nights_quantity']) for folio in folios: folio_dates = cls._get_range_dates(folio, mode='string') dates_target = set(dates_of_month) & folio_dates for dt in dates_target: alldays[dt].append(1) rooms = len(Room.search_read([])) values = [sum(fo_num) * 100 / rooms for _, fo_num in alldays.items()] month_name = today.strftime("%b %Y") label_item = '% Occupancy Rate' res = { 'label_item': label_item, 'labels': labels, 'values': values, 'header_meta': month_name, 'min_value': 1, 'max_value': 100, } return res @classmethod def report_current_occupancy_rate(cls, args): pool = Pool() Room = pool.get('hotel.room') rooms = Room.search_read([ ('active', '=', True), ]) today = date.today() dates_arrival = [] dates_departure = [] first, last = calendar.monthrange(today.year, today.month) day_one = date(today.year, today.month, 1) for md in range(last): dates_arrival.append(day_one + timedelta(days=(md+1))) dates_departure.append(day_one + timedelta(days=(md+2))) rooms_available = len(rooms) * last dom = ['OR', [ ('arrival_date', 'in', dates_arrival), ('booking.state', 'not in', ['no_show', 'cancelled']), ], [ ('departure_date', 'in', dates_departure), ('booking.state', 'not in', ['no_show', 'cancelled']), ] ] folios = cls.search_read(dom, fields_names=[ 'guests', 'arrival_date', 'nights_quantity']) occupation = [] month_dates = set(dates_arrival) for folio in folios: _dates = cls._get_range_dates(folio) occupation.append(len(_dates & month_dates)) value = (sum(occupation) * 100) / rooms_available month_name = today.strftime("%b %Y") value = f'{round(value, 2)}%' res = { 'value': value, 'header_meta': month_name, } return res class FolioGuest(ModelSQL, ModelView): 'Folio Guest' __name__ = 'hotel.folio.guest' REQ_MAIN_GUEST = { 'required': Eval('main_guest', False) } folio = fields.Many2One('hotel.folio', 'Folio') main_guest = fields.Boolean('Main Guest') type_guest = fields.Selection([ ('adult', 'Adult'), ('child', 'Child'), ], 'Type Guest', required=True) type_guest_string = type_guest.translated('type_guest') nationality = fields.Many2One('country.country', 'Nationality') origin_country = fields.Many2One('country.country', 'Origin Country') target_country = fields.Many2One('country.country', 'Target Country') # New fields for speed reason type_document = fields.Selection(TYPE_DOCUMENT, 'Document Type', required=True) type_document_string = type_document.translated('type_document') id_number = fields.Char('Id Number', required=True) name = fields.Char('Name', required=True) mobile = fields.Char('Mobile', states=REQ_MAIN_GUEST) email = fields.Char('Email', states=REQ_MAIN_GUEST) address = fields.Char('Address', states=REQ_MAIN_GUEST) profession = fields.Char('Profession') birthday = fields.Date('Birthday') visa_date = fields.Date('Visa Date') visa_category = fields.Char('Visa Category') visa_number = fields.Char('Visa Number') sex = fields.Selection([ ('female', 'Female'), ('male', 'Male'), ('', ''), ], 'Sex', required=True) sex_string = sex.translated('sex') first_name = fields.Char('First Name') second_name = fields.Char('Second Name') first_family_name = fields.Char('First Family Name') second_family_name = fields.Char('Second Family Name') notes = fields.Text('Notes') country = fields.Many2One('party.country_code', 'Country', states=REQ_MAIN_GUEST) subdivision = fields.Many2One('party.department_code', 'Subdivision') city = fields.Many2One('party.city_code', 'City', domain=[('department', '=', Eval('subdivision'))]) tags = fields.Many2Many('hotel.tag.guest', 'guest', 'tag', 'Tags') siat_send_date = fields.DateTime('Siat Send Date', states={'readonly': True}) siat_id = fields.Char('SIAT Id.', states={'readonly': True}) @classmethod def search_rec_name(cls, name, clause): _, operator, value = clause if operator.startswith('!') or operator.startswith('not '): bool_op = 'AND' else: bool_op = 'OR' domain = [ bool_op, ('id_number', operator, value), ('name', operator, value), ('mobile', operator, value), ] return domain @classmethod def get_splitted_name(cls, name): first_name = None first_family_name = None second_name = None second_family_name = None full_names = { 'second_family_name': None, 'first_family_name': None, 'second_name': None, 'first_name': None, } if name: names = name.split(' ') first_name = names[0] second_family_name = names[-1] if len(names) > 1: first_family_name = names[-2] if len(names) == 2: second_family_name = None first_family_name = names[1] elif len(names) == 5: second_name = names[1] + ' ' + names[2] elif len(names) == 4: second_name = names[1] full_names['second_family_name'] = second_family_name full_names['first_family_name'] = first_family_name full_names['second_name'] = second_name full_names['first_name'] = first_name return full_names @fields.depends('name', 'first_name', 'second_name', 'first_family_name', 'second_family_name') def on_change_name(self): names = self.get_splitted_name(self.name) self.second_family_name = names['second_family_name'] self.first_family_name = names['first_family_name'] self.second_name = names['second_name'] self.first_name = names['first_name'] @staticmethod def default_type_guest(): return 'adult' @staticmethod def default_nationality(): Config = Pool().get('hotel.configuration') config = Config.get_configuration() if config.nationality: return config.nationality.id @staticmethod def default_country(): Config = Pool().get('hotel.configuration') config = Config.get_configuration() if config and config.country: return config.country.id @staticmethod def default_type_document(): return '13' @fields.depends('nationality', 'origin_country', 'target_country') def on_change_nationality(self): if self.nationality: self.target_country = self.nationality.id self.origin_country = self.nationality.id @fields.depends('id_number', 'name', 'sex', 'email', 'mobile', 'visa_number', 'visa_date', 'birthday', 'nationality') def on_change_id_number(self): if self.id_number: Guest = Pool().get('hotel.folio.guest') Party = Pool().get('party.party') guest = None guests = Guest.search([ ('id_number', '=', self.id_number) ]) if guests: guest = guests[0] else: parties = Party.search([ ('id_number', '=', self.id_number) ]) if parties: guest = parties[0] if guest: self.name = guest.name.upper() self.sex = guest.sex self.type_document = guest.type_document self.email = guest.email and guest.email.lower() self.mobile = guest.mobile self.visa_number = guest.visa_number self.visa_date = guest.visa_date self.birthday = guest.birthday self.nationality = guest.nationality and guest.nationality.id self.profession = guest.profession self.first_name = guest.first_name self.second_name = guest.second_name self.first_family_name = guest.first_family_name self.second_family_name = guest.second_family_name self.subdivision = guest.subdivision and guest.subdivision.id self.city = guest.city and guest.city.id address = '' if hasattr(guest, 'address'): address = guest.address and guest.address.upper() elif hasattr(guest, 'addresses') and guest.addresses: address = guest.addresses[0].street self.address = address and address.upper() @classmethod def manage_party(cls, v): Party = Pool().get('party.party') is_main_guest = v.pop('main_guest', False) folio = v.pop('folio', None) party_fields = Party.fields_get().keys() form_fields = set(v.keys()) to_remove = form_fields - party_fields def clean_fields(): # Remove all fields that are not part of the party model for val in to_remove: _ = v.pop(val, None) if is_main_guest: parties = Party.search([ ('id_number', '=', v['id_number']), ]) email = v.pop('email', '').lower() mobile = v.pop('mobile', '').upper() address = v.pop('address', '').upper() nationality = v.pop('nationality') country = v.pop('country', None) subdivision = v.pop('subdivision', None) city = v.pop('city', None) v['type_person'] = 'persona_natural' _ = v.pop('tags', None) if not parties: v['contact_mechanisms'] = [ ('create', [ {'type': 'email', 'value': email}, {'type': 'mobile', 'value': mobile}, ]) ] v['addresses'] = [ ('create', [{ 'street': address, 'country_code': country, 'department_code': subdivision, 'city_code': city, }]) ] # FIXME add tags to party and remove this lines clean_fields() party, = Party.create([v]) else: clean_fields() Party.write(parties, v) party = parties[0] has_email = False has_mobile = False to_write = {} if party.addresses: for addr in party.addresses: addr.street = address addr.country_code = country addr.department_code = subdivision addr.city_code = city addr.save() else: to_write['addresses'] = [('create', [{ 'street': address, 'country_code': nationality, }])] for cm in party.contact_mechanisms: if email and cm.type == 'email': cm.value = email has_email = True elif mobile and cm.type == 'mobile': cm.value = mobile has_mobile = True cm.save() to_write_cm = [] if not has_mobile and mobile: to_write_cm.append({'type': 'mobile', 'value': mobile}) if not has_email and email: to_write_cm.append({'type': 'email', 'value': email}) if to_write_cm: to_write['contact_mechanisms'] = [('create', to_write_cm)] if to_write: Party.write(parties, to_write) if folio: Folio = Pool().get('hotel.folio') folio = Folio(folio) folio.main_guest = party.id folio.save() booking = folio.booking if not booking.party: booking.party = party.id booking.save() @classmethod def create(cls, vlist): for values in vlist: first_name = values.get('first_name', None) if not first_name: names = cls.get_splitted_name(values['name']) values.update(names) res = super(FolioGuest, cls).create(vlist) for v in vlist: cls.manage_party(v) return res @classmethod def write(cls, records, values): super(FolioGuest, cls).write(records, values) fields = cls.fields_get() for val in ('id', 'rec_name', 'write_date', 'write_uid', 'create_date'): fields.pop(val) data = {} fields_list = fields.keys() rec = records[0] for field in fields_list: data[field] = getattr(rec, field) data.update(values) cls.manage_party(data) class FolioCharge(Workflow, ModelSQL, ModelView): 'Folio Charge' __name__ = 'hotel.folio.charge' folio = fields.Many2One('hotel.folio', 'Folio', required=True, ondelete='CASCADE') number = fields.Char('Number', readonly=True, required=False) date_service = fields.Date('Date Service', required=True) product = fields.Many2One('product.product', 'Product', domain=[ ('salable', '=', True), ('active', '=', True) ], required=True) quantity = fields.Integer('Quantity', required=True) invoice_to = fields.Many2One('party.party', 'Invoice To') unit_price = fields.Numeric('Unit Price', digits=(16, 2), required=True) unit_price_w_tax = fields.Function(fields.Numeric('Unit Price w Tax', digits=(16, 2)), 'get_unit_price_w_tax', setter='set_unit_price_w_tax') order = fields.Char('Order') description = fields.Char('Description', depends=['product']) state = fields.Selection(INVOICE_STATES, 'Invoice State', readonly=True) state_string = state.translated('state') invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line', readonly=True) amount = fields.Function(fields.Numeric('Amount', digits=(16, 2)), 'get_amount') amount_w_tax = fields.Function(fields.Numeric('Amount w Tax', digits=(16, 2)), 'get_amount_w_tax') taxed_amount = fields.Function(fields.Numeric('Amount with Tax', digits=(16, 2)), 'get_taxed_amount') kind = fields.Selection([ ('accommodation', 'Accommodation'), ('product', 'Product'), ], 'Kind') # Remove for deprecation to_invoice = fields.Boolean('To Invoice', states={ 'invisible': Bool(Eval('invoice_line')), }, depends=['invoice_line']) ############################ storage = fields.Many2One('stock.location', 'Storage', domain=[ ('type', '=', 'storage')], states={ 'readonly': Bool(Eval('invoice_line')), }) move = fields.Many2One('stock.move', 'Move', states={'readonly': True}) status = fields.Selection([ ('paid', 'Paid'), ('pending', 'Pending'), ], 'Status', help="Status of Payment") status_string = status.translated('status') line_move = fields.Many2One('account.move.line', 'Line Acc. Move') analytic_account = fields.Many2One('analytic_account.account', 'Analytic Account', domain=[ ('type', '=', 'normal') ]) taxes = fields.Many2Many('hotel.folio_charge-account.tax', 'charge', 'tax', 'Taxes', order=[('tax.sequence', 'ASC'), ('tax.id', 'ASC')], domain=[ ('parent', '=', None), [ 'OR', ('group', '=', None), ('group.kind', 'in', ['sale', 'both']) ], ], ) origin = fields.Reference('Origin', selection='get_origin', readonly=True) # @classmethod # def delete(cls, records): # target = [] # for rec in records: # if rec.invoice_line or rec.move: # continue # target.append(rec) # super().delete(target) @classmethod def __setup__(cls): super(FolioCharge, cls).__setup__() cls._buttons.update({ 'bill': { 'invisible': Eval('invoice_state') is not None, }, }) @classmethod def trigger_create(cls, records): cls.set_number(records) @classmethod def set_number(cls, charges): """ Fill the number field with the charge sequence """ pool = Pool() Config = pool.get('hotel.configuration') config = Config.get_configuration() for charge in charges: if charge.number or not config.charge_sequence: continue number = config.charge_sequence.get() cls.write([charge], {'number': number}) @classmethod def _get_origin(cls): 'Return list of Model names for origin Reference' return ['sale.sale', 'sale.line'] @classmethod def get_origin(cls): Model = Pool().get('ir.model') get_name = Model.get_name models = cls._get_origin() return [(None, '')] + [(m, get_name(m)) for m in models] @classmethod def _add_taxes(cls, vlist): pool = Pool() Folio = pool.get('hotel.folio') Product = pool.get('product.product') for value in vlist: folio_id = value.get('folio', None) if folio_id: folio = Folio(folio_id) if folio.taxes_exception: continue product = Product(value['product']) value['taxes'] = [('add', [ tax.id for tax in product.customer_taxes_used ])] @classmethod def _check_create(cls, vlist): Folio = Pool().get('hotel.folio') for value in vlist: folio_id = value.get('folio', None) if folio_id: folio = Folio(folio_id) if folio.charges_blocked: raise UserError(gettext('hotel.msg_folio_charges_blocked')) @classmethod def create(cls, vlist): cls._check_create(vlist) cls._add_taxes(vlist) return super().create(vlist) @staticmethod def default_quantity(): return 1 @staticmethod def default_kind(): return 'product' @staticmethod def default_status(): return 'pending' @staticmethod def default_to_invoice(): return True @staticmethod def default_date_service(): today = Pool().get('ir.date').today() return today def get_amount(self, name=None): res = 0 if self.quantity and self.unit_price: res = self.quantity * self.unit_price return res @classmethod def set_unit_price_w_tax(cls, charges, name, value): Tax = Pool().get('account.tax') to_write = [] rvalue = Decimal(value).quantize(ROUND_TWO) for charge in charges: taxes = charge.product.customer_taxes_used unit_price = Tax.reverse_compute(rvalue, taxes) unit_price = Decimal(unit_price).quantize(ROUND_TWO) to_write.extend([[charge], { 'unit_price': unit_price, }]) cls.write(*to_write) def get_amount_w_tax(self, name=None): res = 0 if self.quantity and self.unit_price_w_tax: res = self.quantity * self.unit_price_w_tax return res def get_unit_price_w_tax(self, name=None): Tax = Pool().get('account.tax') res = self.unit_price or 0 if self.unit_price and not self.folio.taxes_exception: _taxes = Tax.compute( self.product.template.customer_taxes_used, self.unit_price, 1) for tax in _taxes: res += tax['amount'] return round(res, 2) def do_stock_move(self, name=None): pool = Pool() Location = pool.get('stock.location') Move = pool.get('stock.move') Config = Pool().get('hotel.configuration') # FIXME add origin locs_customer = Location.search([ ('type', '=', 'customer') ]) customer_loc_id = locs_customer[0].id config = Config.get_configuration() if not self.storage and config.storage_by_default: storage_id = config.storage_by_default.id else: storage_id = self.storage.id move, = Move.create([{ 'product': self.product.id, 'effective_date': self.date_service, 'quantity': self.quantity, 'unit_price': self.product.cost_price, 'uom': self.product.default_uom.id, 'from_location': storage_id, 'to_location': customer_loc_id, 'origin': str(self), }]) self.move = move.id Move.do([move]) self.save() def get_taxed_amount(self, name=None): res = 0 if self.quantity and self.unit_price_w_tax: res = self.quantity * self.unit_price_w_tax return res @classmethod @ModelView.button def bill(cls, records): cls.create_sales(records) @fields.depends('unit_price', 'folio', 'product', 'description', 'taxes') def on_change_product(self): if self.product: self.unit_price = round(self.product.template.list_price, 2) self.unit_price_w_tax = round(self.product.sale_price_taxed, 2) self.description = self.product.template.name if self.folio and not self.folio.taxes_exception: self.taxes = [ tax.id for tax in self.product.customer_taxes_used ] def get_move_line(self, account, party, amount): debit = credit = _ZERO if amount[0] == 'debit': debit = amount[1] else: credit = amount[1] res = { 'description': self.description, 'debit': debit, 'credit': credit, 'account': account.id, 'party': party.id, } line = self._get_entry(res) return line def get_analytic_lines(self, account, line, date): "Yield analytic lines for the accounting line and the date" lines = [] amount = line['debit'] or line['credit'] for account, amount in account.distribute(amount): analytic_line = {} analytic_line['debit'] = amount if line['debit'] else Decimal(0) analytic_line['credit'] = amount if line['credit'] else Decimal(0) analytic_line['account'] = account analytic_line['date'] = date lines.append(analytic_line) return lines def _get_entry(self, line_move): line_move['analytic_lines'] = [] if self.analytic_account: entry = self.analytic_account to_create = [] # Just debits must to create entries, credits not if entry.account and line_move['credit'] > 0: to_create.extend(self.get_analytic_lines( entry.account, line_move, self.date_service )) if to_create: line_move['analytic_lines'] = [('create', to_create)] return line_move class FolioOccupancy(ModelSQL, ModelView): 'Folio Occupancy' __name__ = 'hotel.folio.occupancy' folio = fields.Many2One('hotel.folio', 'Folio', required=True, ondelete='CASCADE') occupancy_date = fields.Date('Occupancy Date', required=True) charge = fields.Many2One('hotel.folio.charge', 'Charge') price_list = fields.Many2One('hotel.price_list', 'Price List') unit_price = fields.Numeric('Unit Price', digits=(16, 2), states={ 'readonly': True, }) unit_price_w_tax = fields.Function( fields.Numeric('Unit Price W Tax'), 'get_unit_price_w_tax') state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('invoiced', 'Invoiced'), ], 'State', readonly=True) def get_unit_price_w_tax(self, name=None): Tax = Pool().get('account.tax') res = self.unit_price or 0 if self.unit_price and not self.folio.taxes_exception: values = Tax.compute( self.folio.product.template.customer_taxes_used, self.unit_price, 1) if values: value = values[0] res = value['base'] + value['amount'] return res class OpenMigrationStart(ModelView): 'Open Migration Start' __name__ = 'hotel.open_migration.start' start_date = fields.Date('Start Date', required=True) end_date = fields.Date('End Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) @staticmethod def default_company(): return Transaction().context.get('company') class OpenMigration(Wizard): 'Open Migration' __name__ = 'hotel.open_migration' start = StateView( 'hotel.open_migration.start', 'hotel.open_migration_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Open', 'print_', 'tryton-print', default=True), ]) print_ = StateAction('hotel.report_migration') def do_print_(self, action): data = { 'start_date': self.start.start_date, 'end_date': self.start.end_date, 'company': self.start.company.id, } return action, data def transition_print_(self): return 'end' class Migration(Report): 'Hotel Migration' __name__ = 'hotel.migration' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) pool = Pool() Folio = pool.get('hotel.folio') Company = pool.get('company.company') records = Folio.search([ ('arrival_date', '>=', data['start_date']), ('arrival_date', '<=', data['end_date']), ('main_guest', '!=', None), ('registration_state', 'in', ['check_in', 'check_out']), ], order=[('arrival_date', 'ASC')]) end_date = data['end_date'] _records = [] company = Company(data['company']) mig_code = company.mig_code city_code = company.city_code for folio in records: for guest in folio.guests: for move in ('E', 'S'): if move == 'S' and folio.departure_date > end_date: continue if move == 'E': move_date = folio.arrival_date else: move_date = folio.departure_date if not all([ guest.origin_country, guest.target_country, guest.nationality ]): continue _records.append({ 'mig_code': mig_code, 'city_code': city_code, 'type_doc': guest.type_document, 'id_number': guest.id_number, 'nationality': guest.nationality.code_numeric, 'first_family_name': guest.first_family_name, 'second_family_name': guest.second_family_name, 'first_name': guest.first_name, 'move': move, 'move_date': move_date, 'origin': guest.origin_country.code_numeric, 'target': guest.target_country.code_numeric, 'birthday': guest.birthday, }) report_context['records'] = _records return report_context class OperationReport(Report): __name__ = 'hotel.operation' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) user = Pool().get('res.user')(Transaction().user) report_context['company'] = user.company return report_context class CheckOutOperationFailed(ModelView): 'Check Out Operation Failed' __name__ = 'hotel.operation.check_out.failed' class CheckOutOperation(Wizard): 'Check Out Operation' __name__ = 'hotel.operation.check_out' start = StateTransition() failed = StateView( 'hotel.operation.check_out.failed', 'hotel.operation_check_out_failed_view_form', [ Button('Force Check Out', 'force', 'tryton-forward'), Button('Cancel', 'end', 'tryton-cancel', True), ]) force = StateTransition() def transition_start(self): Operation = Pool().get('hotel.operation') active_id = Transaction().context['active_id'] operations = Operation.browse([active_id]) if Operation.validate_check_out_date(operations): return 'end' else: return 'failed' def transition_force(self): Operation = Pool().get('hotel.operation') active_id = Transaction().context['active_id'] operations = Operation.browse([active_id]) Operation.check_out(operations) return 'end' class UpdateOccupancyStart(ModelView): 'Update Occupancy' __name__ = 'hotel.folio_update_occupancy.start' departure_date = fields.Date('Departure Date', states={ 'readonly': Bool(Eval('room')) }) room = fields.Many2One('hotel.room', 'New Room', domain=[ ('id', 'in', Eval('targets')), ]) unit_price = fields.Numeric('Unit Price', digits=(16, 2)) accommodation = fields.Many2One('product.product', 'Accommodation', domain=[ ('template.kind', '=', 'accommodation'), ]) targets = fields.Function(fields.Many2Many('hotel.room', None, None, 'Targets', depends=['departure_date', 'accommodation', 'room']), 'on_change_with_targets') # tranfer_charges = fields.Boolean('Transfer Charges') @fields.depends('departure_date', 'accommodation', 'room') def on_change_with_targets(self, name=None): pool = Pool() RoomTemplate = pool.get('hotel.room-product.template') Folio = pool.get('hotel.folio') folio = Folio(Transaction().context.get('active_id')) res = [] acco = folio.product if self.accommodation: acco = self.accommodation departure_date = folio.departure_date if self.departure_date: departure_date = self.departure_date room_templates = RoomTemplate.search([ ('template.accommodation_capacity', '>=', acco.accommodation_capacity), ('template', '=', acco.template.id) ]) rooms_ids = [t.room.id for t in room_templates] res = Folio.get_available_rooms( folio.arrival_date, departure_date, rooms_ids=rooms_ids ) return res class UpdateOccupancy(Wizard): 'Update Occupancy' __name__ = 'hotel.folio_update_occupancy' """ this is the wizard that allows to the frontdesk employee to change original room o departure date. """ start = StateView( 'hotel.folio_update_occupancy.start', 'hotel.folio_update_occupancy_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Change', 'update', 'tryton-ok'), ] ) update = StateTransition() def default_start(self, fields): targets = self.start.on_change_with_targets() res = { 'targets': targets } return res def transition_update(self): pool = Pool() Folio = pool.get('hotel.folio') folio, = Folio.browse([Transaction().context.get('active_id')]) dep_date = self.start.departure_date price = self.start.unit_price room = self.start.room folio.update_occupancy(dep_date=dep_date, price=price) if dep_date: folio.departure_date = dep_date if price: folio.unit_price = price if room: folio.room = room.id folio.save() return 'end' class OperationByConsumerReport(Report): __name__ = 'hotel.folio.charge' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) pool = Pool() Folio = pool.get('hotel.folio') user = pool.get('res.user')(Transaction().user) consumer_lines = [] total_amount = 0 # if records: # line = records[0] # folio = Folio(line.folio.id) # total_amount = folio.room_amount # for ch in folio.lines: # if ch.invoice_to and ch.invoice_to.id == line.invoice_to.id: # consumer_lines.append(l) # total_amount += l.amount # setattr(folio, 'lines', consumer_lines) # setattr(folio, 'total_amount', total_amount) # # report_context['records'] = [folio] report_context['company'] = user.company return report_context class OperationBill(Wizard): 'Operation Bill' __name__ = 'hotel.folio.bill' start_state = 'create_bill' create_bill = StateTransition() def transition_create_bill(self): Folio = Pool().get('hotel.folio') ids = Transaction().context['active_ids'] folios = Folio.browse(ids) Folio.bill(folios) return 'end' class OperationVoucher(ModelSQL): 'Operation - Voucher' __name__ = 'hotel.folio-account.voucher' _table = 'folio_vouchers_rel' folio = fields.Many2One('hotel.folio', 'Folio', ondelete='CASCADE', required=True) voucher = fields.Many2One('account.voucher', 'Voucher', required=True, domain=[('voucher_type', '=', 'receipt')], ondelete='RESTRICT') @classmethod def set_voucher_origin(cls, voucher_id, folio_id): cls.create([{ 'voucher': voucher_id, 'folio': folio_id, }]) class ReverseCheckout(Wizard): 'Reverse Checkout' __name__ = 'hotel.folio.reverse_checkout' start_state = 'reverse_checkout' reverse_checkout = StateTransition() def transition_reverse_checkout(self): Folio = Pool().get('hotel.folio') folio_id = Transaction().context['active_id'] if folio_id: Folio.check_in([Folio(folio_id)]) return 'end' class FolioStockMove(ModelSQL): 'Folio - Stock Move' __name__ = 'hotel.folio-stock.move' _table = 'hotel_folio_stock_move_rel' folio = fields.Many2One('hotel.folio', 'Folio', ondelete='CASCADE', required=True) move = fields.Many2One('stock.move', 'Move', ondelete='RESTRICT', required=True) class StatisticsByMonthStart(ModelView): 'Statistics By Month Start' __name__ = 'hotel.statistics_by_month.start' period = fields.Many2One('account.period', 'Period', required=True) company = fields.Many2One('company.company', 'Company', required=True) @staticmethod def default_company(): return Transaction().context.get('company') class StatisticsByMonth(Wizard): 'Statistics By Month' __name__ = 'hotel.statistics_by_month' start = StateView( 'hotel.statistics_by_month.start', 'hotel.print_hotel_statistics_by_month_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Open', 'print_', 'tryton-print', default=True), ]) print_ = StateAction('hotel.statistics_by_month_report') def do_print_(self, action): data = { 'period': self.start.period.id, 'company': self.start.company.id, } return action, data def transition_print_(self): return 'end' class StatisticsByMonthReport(Report): 'Hotel Statistics By Month' __name__ = 'hotel.statistics_by_month.report' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) Operation = Pool().get('hotel.folio') Period = Pool().get('account.period') Company = Pool().get('company.company') Rooms = Pool().get('hotel.room') period = Period(data['period']) company = Company(data['company']) month_days = (period.end_date - period.start_date).days + 1 rooms_available = [] rooms_sold = [] beds_available = [] beds_sold = [] local_guests = [] foreign_guests = [] room1_sold = [] room2_sold = [] room_suite_sold = [] room_other_sold = [] guests_one_ng = [] guests_two_ng = [] guests_three_ng = [] guests_four_ng = [] guests_five_ng = [] guests_six_ng = [] guests_seven_ng = [] guests_eight_ng = [] guests_permanent_ng = [] reason_bussiness = [] reason_bussiness_foreign = [] reason_leisure = [] reason_leisure_foreign = [] reason_convention = [] reason_convention_foreign = [] reason_health = [] reason_health_foreign = [] reason_transport = [] reason_transport_foreign = [] reason_other = [] reason_other_foreign = [] total_guests = [] folios = Operation.search(['OR', [ ('start_date', '>=', period.start_date), ('start_date', '<=', period.end_date), ('kind', '=', 'occupancy'), ], [ ('end_date', '>=', period.start_date), ('end_date', '<=', period.end_date), ('kind', '=', 'occupancy'), ] ]) rooms = Rooms.search([]) rooms_available = [len(rooms) * month_days] for r in rooms: beds_available.append( r.main_accommodation.accommodation_capacity * month_days ) def _get_ctx_dates(op): start_date = op.start_date end_date = op.end_date if op.start_date < period.start_date: start_date = period.start_date if op.end_date > period.end_date: end_date = period.end_date sub_dates = [ str(start_date + timedelta(n)) for n in range((end_date - start_date).days) ] return sub_dates for op in folios: room_capacity = op.room.main_accommodation.accommodation_capacity ctx_dates = _get_ctx_dates(op) len_ctx_dates = len(ctx_dates) rooms_sold.append(len_ctx_dates) beds_sold.append(len_ctx_dates * room_capacity) qty_guests = len(op.guests) if not qty_guests: qty_guests = 1 total_guests.append(qty_guests) is_local = True if op.main_guest.type_document == '41': is_local = False foreign_guests.append(qty_guests) else: local_guests.append(qty_guests) if room_capacity == 1: room1_sold.append(qty_guests) elif room_capacity == 2: room2_sold.append(qty_guests) elif room_capacity > 2: room_suite_sold.append(qty_guests) if len_ctx_dates == 1: guests_one_ng.append(qty_guests) elif len_ctx_dates == 2: guests_two_ng.append(qty_guests) elif len_ctx_dates == 3: guests_three_ng.append(qty_guests) elif len_ctx_dates == 4: guests_four_ng.append(qty_guests) elif len_ctx_dates == 5: guests_five_ng.append(qty_guests) elif len_ctx_dates == 6: guests_six_ng.append(qty_guests) elif len_ctx_dates == 7: guests_seven_ng.append(qty_guests) elif len_ctx_dates >= 8: guests_eight_ng.append(qty_guests) segment = 'bussiness' if op.origin: if hasattr(op.origin, 'booking'): segment = op.origin.booking.segment if segment == 'bussiness': if is_local: reason_bussiness.append(qty_guests) else: reason_bussiness_foreign.append(qty_guests) elif segment == 'convention': if is_local: reason_convention.append(qty_guests) else: reason_convention_foreign.append(qty_guests) elif segment == 'health': if is_local: reason_health.append(qty_guests) else: reason_health_foreign.append(qty_guests) else: if is_local: reason_leisure.append(qty_guests) else: reason_leisure_foreign.append(qty_guests) def _get_rate(val): res = 0 if sum_total_guests > 0: res = round(float(sum(val)) / sum_total_guests, 4) return res sum_total_guests = sum(total_guests) rate_guests_one_ng = _get_rate(guests_one_ng) rate_guests_two_ng = _get_rate(guests_two_ng) rate_guests_three_ng = _get_rate(guests_three_ng) rate_guests_four_ng = _get_rate(guests_four_ng) rate_guests_five_ng = _get_rate(guests_five_ng) rate_guests_six_ng = _get_rate(guests_six_ng) rate_guests_seven_ng = _get_rate(guests_seven_ng) rate_guests_eight_ng = _get_rate(guests_eight_ng) report_context['period'] = period.name report_context['company'] = company.party.name report_context['rooms_available'] = sum(rooms_available) report_context['rooms_sold'] = sum(rooms_sold) report_context['beds_available'] = sum(beds_available) report_context['beds_sold'] = sum(beds_sold) report_context['local_guests'] = sum(local_guests) report_context['foreign_guests'] = sum(foreign_guests) report_context['room1_sold'] = sum(room1_sold) report_context['room2_sold'] = sum(room2_sold) report_context['room_suite_sold'] = sum(room_suite_sold) report_context['room_other_sold'] = sum(room_other_sold) report_context['guests_one_ng'] = rate_guests_one_ng report_context['guests_two_ng'] = rate_guests_two_ng report_context['guests_three_ng'] = rate_guests_three_ng report_context['guests_four_ng'] = rate_guests_four_ng report_context['guests_five_ng'] = rate_guests_five_ng report_context['guests_six_ng'] = rate_guests_six_ng report_context['guests_seven_ng'] = rate_guests_seven_ng report_context['guests_eight_ng'] = rate_guests_eight_ng report_context['guests_permanent_ng'] = sum(guests_permanent_ng) report_context['reason_bussiness'] = sum(reason_bussiness) report_context['reason_bussiness_foreign'] = sum(reason_bussiness_foreign) report_context['reason_leisure'] = sum(reason_leisure) report_context['reason_leisure_foreign'] = sum(reason_leisure_foreign) report_context['reason_convention'] = sum(reason_convention) report_context['reason_convention_foreign'] = sum(reason_convention_foreign) report_context['reason_health'] = sum(reason_health) report_context['reason_health_foreign'] = sum(reason_health_foreign) report_context['reason_transport'] = sum(reason_transport) report_context['reason_transport_foreign'] = sum(reason_transport_foreign) report_context['reason_other_foreign'] = sum(reason_other_foreign) report_context['reason_other'] = sum(reason_other) return report_context class GuestsListStart(ModelView): 'Guests List Start' __name__ = 'hotel.print_guests_list.start' date = fields.Date('Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) @staticmethod def default_date(): Date_ = Pool().get('ir.date') return Date_.today() @staticmethod def default_company(): return Transaction().context.get('company') class GuestsList(Wizard): 'Guests List' __name__ = 'hotel.print_guests_list' start = StateView( 'hotel.print_guests_list.start', 'hotel.print_guests_list_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Open', 'print_', 'tryton-print', default=True), ]) print_ = StateReport('hotel.guests_list.report') def do_print_(self, action): company = self.start.company data = { 'date': self.start.date, 'company': company.id, } return action, data def transition_print_(self): return 'end' class GuestsListReport(Report): __name__ = 'hotel.guests_list.report' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) pool = Pool() Company = pool.get('company.company') Folio = pool.get('hotel.folio') Room = pool.get('hotel.room') User = pool.get('res.user') user_id = Transaction().user user = User(user_id) dom = [ ('arrival_date', '<=', data['date']), ('departure_date', '>', data['date']), ] folios = Folio.search(dom, order=[('room.code', 'ASC')]) total_guests = [] rooms_occupied = [] for op in folios: total_guests.append(len(op.guests)) rooms_occupied.append(op.room.id) rooms = Room.search_read([]) _rooms_occupied = len(set(rooms_occupied)) num_rooms = len(rooms) if num_rooms: occupancy_rate = round(_rooms_occupied / num_rooms, 2) else: occupancy_rate = 0 guests = [] for fo in folios: for guest in fo.guests: guests.append({ 'room': fo.room.code, 'guest': guest.name, 'party': fo.booking.party.name if fo.booking.party else '', 'arrival_date': fo.arrival_date, 'departure_date': fo.departure_date, 'nights_quantity': fo.nights_quantity, 'state': fo.registration_state, }) report_context['records'] = guests report_context['total_rooms'] = _rooms_occupied report_context['total_guests'] = sum(total_guests) report_context['occupancy_rate'] = occupancy_rate report_context['company'] = Company(data['company']) report_context['date'] = data['date'] report_context['print_date'] = datetime.now() report_context['user'] = user.name return report_context class RegistrationCardReport(Report): __name__ = 'hotel.folio.registration_card' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) user = Pool().get('res.user')(Transaction().user) report_context['company'] = user.company _records = [] for rec in records: if not rec.registration_card: continue _records.append(rec) report_context['records'] = _records return report_context class HotelFolioTax(ModelSQL): 'Hotel Folio - Tax' __name__ = 'hotel.folio-account.tax' _table = 'hotel_folio_account_tax' folio = fields.Many2One('hotel.folio', 'Folio', ondelete='CASCADE', required=True) tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT', required=True) class HotelFolioChargeTax(ModelSQL): 'Hotel Folio Charge - Tax' __name__ = 'hotel.folio_charge-account.tax' _table = 'hotel_folio_charge_account_tax' charge = fields.Many2One('hotel.folio.charge', 'Charge', ondelete='CASCADE', required=True) tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT', required=True) class FolioAuditStart(ModelView): 'Folio Audit Start' __name__ = 'hotel.folio_audit.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) registration_state = fields.Selection([ ('check_in', 'Check In'), ('check_out', 'Check Out'), ('', ''), ], 'Reg. State') @staticmethod def default_company(): return Transaction().context.get('company') class FolioAudit(Wizard): 'Folio Audit' __name__ = 'hotel.folio_audit' start = StateView( 'hotel.folio_audit.start', 'hotel.print_folio_audit_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Open', 'print_', 'tryton-print', default=True), ]) print_ = StateReport('hotel.folio_audit.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.registration_state, } return action, data def transition_print_(self): return 'end' class FolioAuditReport(Report): __name__ = 'hotel.folio_audit.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( ('state', '=', data['state']) ) _records = Folio.search(dom, order=[('arrival_date', 'ASC')]) report_context['records'] = _records report_context['company'] = Company(data['company']) return report_context