# -*- coding: utf-8 -*- ################################################################################ # # Cybrosys Technologies Pvt. Ltd. # # Copyright (C) 2023-TODAY Cybrosys Technologies(). # Author: Unnimaya C O (odoo@cybrosys.com) # # You can modify it under the terms of the GNU LESSER # GENERAL PUBLIC LICENSE (LGPL v3), Version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. # # You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE # (LGPL v3) along with this program. # If not, see . # ################################################################################ from datetime import datetime, timedelta from odoo import api, fields, models, _ from odoo.exceptions import ValidationError from odoo.tools.safe_eval import pytz class RoomBooking(models.Model): """Model that handles the hotel room booking and all operations related to booking""" _name = "room.booking" _description = "Hotel Room Reservation" _inherit = ['mail.thread', 'mail.activity.mixin'] name = fields.Char(string="Folio Number", readonly=True, index=True, default="New", help="Name of Folio") company_id = fields.Many2one('res.company', string="Company", help="Choose the Company", required=True, index=True, default=lambda self: self.env.company) partner_id = fields.Many2one('res.partner', string="Customer", help="Customers of hotel", required=True, index=True, tracking=1, domain="[('type', '!=', 'private')," " ('company_id', 'in', " "(False, company_id))]") date_order = fields.Datetime( string="Order Date", required=True, copy=False, help="Creation date of draft/sent orders," " Confirmation date of confirmed orders.", default=fields.Datetime.now) is_checkin = fields.Boolean(default=False, string="Is Checkin", help="sets to True if the room is occupied") maintenance_request_sent = fields.Boolean(default=False, string="Maintenance Request sent " "or Not", help="sets to True if the " "maintenance request send " "once") checkin_date = fields.Datetime(string="Check In", help="Date of Checkin", default=fields.Datetime.now()) checkout_date = fields.Datetime(string="Check Out", help="Date of Checkout", states={"draft": [("readonly", False)]}, default=fields.Datetime.now() + timedelta( hours=23, minutes=59, seconds=59)) hotel_policy = fields.Selection( [("prepaid", "On Booking"), ("manual", "On Check In"), ("picking", "On Checkout"), ], default="manual", string="Hotel Policy", help="Hotel policy for payment that " "either the guest has to pay at " "booking time, check-in " "or check-out time.", tracking=True ) duration = fields.Integer(string="Duration in Days", help="Number of days which will automatically " "count from the check-in and check-out " "date.", ) invoice_button_visible = fields.Boolean(string='Invoice Button Display', help="Invoice button will be " "visible if this button is " "True") invoice_status = fields.Selection( selection=[('no_invoice', 'Nothing To Invoice'), ('to_invoice', 'To Invoice'), ('invoiced', 'Invoiced'), ], string="Invoice Status", help="Status of the Invoice", default='no_invoice', tracking=True) hotel_invoice_id = fields.Many2one("account.move", string="Invoice", help="Indicates the invoice", copy=False) duration_visible = fields.Float(string="Duration", help="A dummy field for Duration") need_service = fields.Boolean(default=False, string="Need Service", help="Check if a Service to be added with" " the Booking") need_fleet = fields.Boolean(default=False, string="Need Vehicle", help="Check if a Fleet to be" " added with the Booking") need_food = fields.Boolean(default=False, string="Need Food", help="Check if a Food to be added with" " the Booking") need_event = fields.Boolean(default=False, string="Need Event", help="Check if a Event to be added with" " the Booking") service_line_ids = fields.One2many("service.booking.line", "booking_id", string="Service", help="Hotel services details provided to" "Customer and it will included in " "the main Invoice.") event_line_ids = fields.One2many("event.booking.line", 'booking_id', string="Event", help="Hotel event reservation detail.") vehicle_line_ids = fields.One2many("fleet.booking.line", "booking_id", string="Vehicle", help="Hotel fleet reservation detail.") room_line_ids = fields.One2many("room.booking.line", "booking_id", string="Room", help="Hotel room reservation detail.") food_order_line_ids = fields.One2many("food.booking.line", "booking_id", string='Food', help="Food details provided" " to Customer and" " it will included in the " "main invoice.", ) state = fields.Selection(selection=[('draft', 'Draft'), ('reserved', 'Reserved'), ('check_in', 'Check In'), ('check_out', 'Check Out'), ('cancel', 'Cancelled'), ('done', 'Done')], string='State', help="State of the Booking", default='draft', tracking=True) user_id = fields.Many2one( comodel_name='res.partner', string="Invoice Address", compute='_compute_user_id', help="Sets the User automatically", required=True, domain="['|', ('company_id', '=', False), ('company_id', '='," " company_id)]") pricelist_id = fields.Many2one( comodel_name='product.pricelist', string="Pricelist", compute='_compute_pricelist_id', store=True, readonly=False, required=True, tracking=1, help="If you change the pricelist, only newly added lines" " will be affected.") currency_id = fields.Many2one( string="Currency", help="This is the Currency used", related='pricelist_id.currency_id', depends=['pricelist_id.currency_id'], ) invoice_count = fields.Integer(compute='_compute_invoice_count', string="Invoice " "Count", help="The number of invoices created") account_move = fields.Integer(string='Invoice Id', help="Id of the invoice created") amount_untaxed = fields.Monetary(string="Total Untaxed Amount", help="This indicates the total untaxed " "amount", store=True, compute='_compute_amount_untaxed', tracking=5) amount_tax = fields.Monetary(string="Taxes", help="Total Tax Amount", store=True, compute='_compute_amount_untaxed') amount_total = fields.Monetary(string="Total", store=True, help="The total Amount including Tax", compute='_compute_amount_untaxed', tracking=4) amount_untaxed_room = fields.Monetary(string="Room Untaxed", help="Untaxed Amount for Room", compute='_compute_amount_untaxed', tracking=5) amount_untaxed_food = fields.Monetary(string="Food Untaxed", help="Untaxed Amount for Food", compute='_compute_amount_untaxed', tracking=5) amount_untaxed_event = fields.Monetary(string="Event Untaxed", help="Untaxed Amount for Event", compute='_compute_amount_untaxed', tracking=5) amount_untaxed_service = fields.Monetary( string="Service Untaxed", help="Untaxed Amount for Service", compute='_compute_amount_untaxed', tracking=5) amount_untaxed_fleet = fields.Monetary(string="Amount Untaxed", help="Untaxed amount for Fleet", compute='_compute_amount_untaxed', tracking=5) amount_taxed_room = fields.Monetary(string="Rom Tax", help="Tax for Room", compute='_compute_amount_untaxed', tracking=5) amount_taxed_food = fields.Monetary(string="Food Tax", help="Tax for Food", compute='_compute_amount_untaxed', tracking=5) amount_taxed_event = fields.Monetary(string="Event Tax", help="Tax for Event", compute='_compute_amount_untaxed', tracking=5) amount_taxed_service = fields.Monetary(string="Service Tax", compute='_compute_amount_untaxed', help="Tax for Service", tracking=5) amount_taxed_fleet = fields.Monetary(string="Fleet Tax", compute='_compute_amount_untaxed', help="Tax for Fleet", tracking=5) amount_total_room = fields.Monetary(string="Total Amount for Room", compute='_compute_amount_untaxed', help="This is the Total Amount for " "Room", tracking=5) amount_total_food = fields.Monetary(string="Total Amount for Food", compute='_compute_amount_untaxed', help="This is the Total Amount for " "Food", tracking=5) amount_total_event = fields.Monetary(string="Total Amount for Event", compute='_compute_amount_untaxed', help="This is the Total Amount for " "Event", tracking=5) amount_total_service = fields.Monetary(string="Total Amount for Service", compute='_compute_amount_untaxed', help="This is the Total Amount for " "Service", tracking=5) amount_total_fleet = fields.Monetary(string="Total Amount for Fleet", compute='_compute_amount_untaxed', help="This is the Total Amount for " "Fleet", tracking=5) def unlink(self): for rec in self: if rec.state != 'cancel' and rec.state != 'draft': raise ValidationError('Cannot delete the Booking. Cancel the Booking ') return super().unlink() @api.model def create(self, vals_list): """Sequence Generation""" if vals_list.get('name', 'New') == 'New': vals_list['name'] = self.env['ir.sequence'].next_by_code( 'room.booking') return super().create(vals_list) @api.depends('partner_id') def _compute_user_id(self): """Computes the User id""" for order in self: order.user_id = \ order.partner_id.address_get(['invoice'])[ 'invoice'] if order.partner_id else False def _compute_invoice_count(self): """Compute the invoice count""" for record in self: record.invoice_count = self.env['account.move'].search_count( [('ref', '=', self.name)]) @api.depends('partner_id') def _compute_pricelist_id(self): """Computes PriceList""" for order in self: if not order.partner_id: order.pricelist_id = False continue order = order.with_company(order.company_id) order.pricelist_id = order.partner_id.property_product_pricelist @api.depends('room_line_ids.price_subtotal', 'room_line_ids.price_tax', 'room_line_ids.price_total', 'food_order_line_ids.price_subtotal', 'food_order_line_ids.price_tax', 'food_order_line_ids.price_total', 'service_line_ids.price_subtotal', 'service_line_ids.price_tax', 'service_line_ids.price_total', 'vehicle_line_ids.price_subtotal', 'vehicle_line_ids.price_tax', 'vehicle_line_ids.price_total', 'event_line_ids.price_subtotal', 'event_line_ids.price_tax', 'event_line_ids.price_total', ) def _compute_amount_untaxed(self, flag=False): """Compute the total amounts of the Sale Order""" amount_untaxed_room = 0.0 amount_untaxed_food = 0.0 amount_untaxed_fleet = 0.0 amount_untaxed_event = 0.0 amount_untaxed_service = 0.0 amount_taxed_room = 0.0 amount_taxed_food = 0.0 amount_taxed_fleet = 0.0 amount_taxed_event = 0.0 amount_taxed_service = 0.0 amount_total_room = 0.0 amount_total_food = 0.0 amount_total_fleet = 0.0 amount_total_event = 0.0 amount_total_service = 0.0 room_lines = self.room_line_ids food_lines = self.food_order_line_ids service_lines = self.service_line_ids fleet_lines = self.vehicle_line_ids event_lines = self.event_line_ids booking_list = [] account_move_line = self.env['account.move.line'].search_read( domain=[('ref', '=', self.name), ('display_type', '!=', 'payment_term')], fields=['name', 'quantity', 'price_unit', 'product_type'], ) for rec in account_move_line: del rec['id'] if room_lines: amount_untaxed_room += sum(room_lines.mapped('price_subtotal')) amount_taxed_room += sum(room_lines.mapped('price_tax')) amount_total_room += sum(room_lines.mapped('price_total')) for room in room_lines: booking_dict = {'name': room.room_id.name, 'quantity': room.uom_qty, 'price_unit': room.price_unit, 'product_type': 'room'} if booking_dict not in account_move_line: if not account_move_line: booking_list.append(booking_dict) else: for rec in account_move_line: if rec['product_type'] == 'room': if booking_dict['name'] == rec['name'] and \ booking_dict['price_unit'] == rec[ 'price_unit'] and booking_dict['quantity'] \ != rec['quantity']: booking_list.append( {'name': room.room_id.name, "quantity": booking_dict[ 'quantity'] - rec[ 'quantity'], "price_unit": room.price_unit, "product_type": 'room'}) else: booking_list.append(booking_dict) if flag: room.booking_line_visible = True if food_lines: for food in food_lines: booking_list.append(self.create_list(food)) amount_untaxed_food += sum(food_lines.mapped('price_subtotal')) amount_taxed_food += sum(food_lines.mapped('price_tax')) amount_total_food += sum(food_lines.mapped('price_total')) if service_lines: for service in service_lines: booking_list.append(self.create_list(service)) amount_untaxed_service += sum( service_lines.mapped('price_subtotal')) amount_taxed_service += sum(service_lines.mapped('price_tax')) amount_total_service += sum(service_lines.mapped('price_total')) if fleet_lines: for fleet in fleet_lines: booking_list.append(self.create_list(fleet)) amount_untaxed_fleet += sum(fleet_lines.mapped('price_subtotal')) amount_taxed_fleet += sum(fleet_lines.mapped('price_tax')) amount_total_fleet += sum(fleet_lines.mapped('price_total')) if event_lines: for event in event_lines: booking_list.append(self.create_list(event)) amount_untaxed_event += sum(event_lines.mapped('price_subtotal')) amount_taxed_event += sum(event_lines.mapped('price_tax')) amount_total_event += sum(event_lines.mapped('price_total')) for rec in self: rec.amount_untaxed = amount_untaxed_food + amount_untaxed_room + \ amount_untaxed_fleet + \ amount_untaxed_event + amount_untaxed_service rec.amount_untaxed_food = amount_untaxed_food rec.amount_untaxed_room = amount_untaxed_room rec.amount_untaxed_fleet = amount_untaxed_fleet rec.amount_untaxed_event = amount_untaxed_event rec.amount_untaxed_service = amount_untaxed_service rec.amount_tax = (amount_taxed_food + amount_taxed_room + amount_taxed_fleet + amount_taxed_event + amount_taxed_service) rec.amount_taxed_food = amount_taxed_food rec.amount_taxed_room = amount_taxed_room rec.amount_taxed_fleet = amount_taxed_fleet rec.amount_taxed_event = amount_taxed_event rec.amount_taxed_service = amount_taxed_service rec.amount_total = (amount_total_food + amount_total_room + amount_total_fleet + amount_total_event + amount_total_service) rec.amount_total_food = amount_total_food rec.amount_total_room = amount_total_room rec.amount_total_fleet = amount_total_fleet rec.amount_total_event = amount_total_event rec.amount_total_service = amount_total_service return booking_list @api.onchange('need_food') def _onchange_need_food(self): """Unlink Food Booking Line if Need Food is false""" if not self.need_food and self.food_order_line_ids: for food in self.food_order_line_ids: food.unlink() @api.onchange('need_service') def _onchange_need_service(self): """Unlink Service Booking Line if Need Service is False""" if not self.need_service and self.service_line_ids: for serv in self.service_line_ids: serv.unlink() @api.onchange('need_fleet') def _onchange_need_fleet(self): """Unlink Fleet Booking Line if Need Fleet is False""" if not self.need_fleet: if self.vehicle_line_ids: for fleet in self.vehicle_line_ids: fleet.unlink() @api.onchange('need_event') def _onchange_need_event(self): """Unlink Event Booking Line if Need Event is False""" if not self.need_event: if self.event_line_ids: for event in self.event_line_ids: event.unlink() @api.onchange('food_order_line_ids', 'room_line_ids', 'service_line_ids', 'vehicle_line_ids', 'event_line_ids') def _onchange_room_line_ids(self): """Invokes the Compute amounts function""" self._compute_amount_untaxed() self.invoice_button_visible = False @api.constrains("room_line_ids") def _check_duplicate_folio_room_line(self): """ This method is used to validate the room_lines. ------------------------------------------------ @param self: object pointer @return: raise warning depending on the validation """ for record in self: # Create a set of unique ids ids = set() for line in record.room_line_ids: if line.room_id.id in ids: raise ValidationError( _( """Room Entry Duplicates Found!, """ """You Cannot Book "%s" Room More Than Once!""" ) % line.room_id.name ) ids.add(line.room_id.id) def create_list(self, line_ids): """Returns a Dictionary containing the Booking line Values""" account_move_line = self.env['account.move.line'].search_read( domain=[('ref', '=', self.name), ('display_type', '!=', 'payment_term')], fields=['name', 'quantity', 'price_unit', 'product_type'], ) for rec in account_move_line: del rec['id'] booking_dict = {} for line in line_ids: name = "" product_type = "" if line_ids._name == 'food.booking.line': name = line.food_id.name product_type = 'food' elif line_ids._name == 'fleet.booking.line': name = line.fleet_id.name product_type = 'fleet' elif line_ids._name == 'service.booking.line': name = line.service_id.name product_type = 'service' elif line_ids._name == 'event.booking.line': name = line.event_id.name product_type = 'event' booking_dict = {'name': name, 'quantity': line.uom_qty, 'price_unit': line.price_unit, 'product_type': product_type} return booking_dict def action_reserve(self): """Button Reserve Function""" if self.state == 'reserved': message = _("Room Already Reserved.") return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'type': 'warning', 'message': message, 'next': {'type': 'ir.actions.act_window_close'}, } } if self.room_line_ids: for room in self.room_line_ids: room.room_id.write({ 'status': 'reserved', }) room.room_id.is_room_avail = False self.write({"state": "reserved"}) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'type': 'success', 'message': "Rooms reserved Successfully!", 'next': {'type': 'ir.actions.act_window_close'}, } } raise ValidationError(_("Please Enter Room Details")) def action_cancel(self): """ @param self: object pointer """ if self.room_line_ids: for room in self.room_line_ids: room.room_id.write({ 'status': 'available', }) room.room_id.is_room_avail = True self.write({"state": "cancel"}) def action_maintenance_request(self): """ Function that handles the maintenance request """ room_list = [] for rec in self.room_line_ids.room_id.ids: room_list.append(rec) if room_list: room_id = self.env['hotel.room'].search([ ('id', 'in', room_list)]) self.env['maintenance.request'].sudo().create({ 'date': fields.Date.today(), 'state': 'draft', 'type': 'room', 'room_maintenance_ids': room_id.ids, }) self.maintenance_request_sent = True return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'type': 'success', 'message': "Maintenance Request Sent Successfully", 'next': {'type': 'ir.actions.act_window_close'}, } } raise ValidationError(_("Please Enter Room Details")) def action_done(self): """Button action_confirm function""" for rec in self.env['account.move'].search( [('ref', '=', self.name)]): if rec.payment_state != 'not_paid': self.write({"state": "done"}) self.is_checkin = False if self.room_line_ids: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'type': 'success', 'message': "Booking Checked Out Successfully!", 'next': {'type': 'ir.actions.act_window_close'}, } } raise ValidationError(_('Your Invoice is Due for Payment.')) self.write({"state": "done"}) def action_checkout(self): """Button action_heck_out function""" self.write({"state": "check_out"}) for room in self.room_line_ids: room.room_id.write({ 'status': 'available', 'is_room_avail': True }) room.write({'checkout_date': datetime.today()}) def action_invoice(self): """Method for creating invoice""" if not self.room_line_ids: raise ValidationError(_("Please Enter Room Details")) booking_list = self._compute_amount_untaxed(True) if booking_list: account_move = self.env["account.move"].create([{ 'move_type': 'out_invoice', 'invoice_date': fields.Date.today(), 'partner_id': self.partner_id.id, 'ref': self.name, }]) for rec in booking_list: account_move.invoice_line_ids.create([{ 'name': rec['name'], 'quantity': rec['quantity'], 'price_unit': rec['price_unit'], 'move_id': account_move.id, 'price_subtotal': rec['quantity'] * rec['price_unit'], 'product_type': rec['product_type'], }]) self.write({'invoice_status': "invoiced"}) self.invoice_button_visible = True return { 'type': 'ir.actions.act_window', 'name': 'Invoices', 'view_mode': 'form', 'view_type': 'form', 'res_model': 'account.move', 'view_id': self.env.ref('account.view_move_form').id, 'res_id': account_move.id, 'context': "{'create': False}" } def action_view_invoices(self): """Method for Returning invoice View""" return { 'type': 'ir.actions.act_window', 'name': 'Invoices', 'view_mode': 'tree,form', 'view_type': 'tree,form', 'res_model': 'account.move', 'domain': [('ref', '=', self.name)], 'context': "{'create': False}" } def action_checkin(self): """ @param self: object pointer """ if not self.room_line_ids: raise ValidationError(_("Please Enter Room Details")) else: for room in self.room_line_ids: room.room_id.write({ 'status': 'occupied', }) room.room_id.is_room_avail = False self.write({"state": "check_in"}) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'type': 'success', 'message': "Booking Checked In Successfully!", 'next': {'type': 'ir.actions.act_window_close'}, } } def get_details(self): """ Returns different counts for displaying in dashboard""" today = datetime.today() tz_name = self.env.user.tz today_utc = pytz.timezone('UTC').localize(today, is_dst=False) context_today = today_utc.astimezone(pytz.timezone(tz_name)) total_room = self.env['hotel.room'].search_count([]) check_in = self.env['room.booking'].search_count( [('state', '=', 'check_in')]) available_room = self.env['hotel.room'].search( [('status', '=', 'available')]) reservation = self.env['room.booking'].search_count( [('state', '=', 'reserved')]) check_outs = self.env['room.booking'].search([]) check_out = 0 staff = 0 for rec in check_outs: for room in rec.room_line_ids: if room.checkout_date.date() == context_today.date(): check_out += 1 """staff""" staff = self.env['res.users'].search_count( [('groups_id', 'in', [self.env.ref('hotel_management_odoo.hotel_group_admin').id, self.env.ref( 'hotel_management_odoo.cleaning_team_group_head').id, self.env.ref( 'hotel_management_odoo.cleaning_team_group_user').id, self.env.ref( 'hotel_management_odoo.hotel_group_reception').id, self.env.ref( 'hotel_management_odoo.maintenance_team_group_leader').id, self.env.ref( 'hotel_management_odoo.maintenance_team_group_user').id ])]) total_vehicle = self.env['fleet.vehicle.model'].search_count([]) available_vehicle = total_vehicle - self.env[ 'fleet.booking.line'].search_count( [('state', '=', 'check_in')]) total_event = self.env['event.event'].search_count([]) pending_event = self.env['event.event'].search([]) pending_events = 0 today_events = 0 for pending in pending_event: if pending.date_end >= fields.datetime.now(): pending_events += 1 if pending.date_end.date() == fields.date.today(): today_events += 1 food_items = self.env['lunch.product'].search_count([]) food_order = len(self.env['food.booking.line'].search([]).filtered( lambda r: r.booking_id.state not in ['check_out', 'cancel', 'done'])) """total Revenue""" total_revenue = 0 today_revenue = 0 pending_payment = 0 for rec in self.env['account.move'].search( [('payment_state', '=', 'paid')]): if rec.ref: if 'BOOKING' in rec.ref: total_revenue += rec.amount_total if rec.date == fields.date.today(): today_revenue += rec.amount_total for rec in self.env['account.move'].search( [('payment_state', '=', 'not_paid')]): if rec.ref: if 'BOOKING' in rec.ref: pending_payment += rec.amount_total return { 'total_room': total_room, 'available_room': len(available_room), 'staff': staff, 'check_in': check_in, 'reservation': reservation, 'check_out': check_out, 'total_vehicle': total_vehicle, 'available_vehicle': available_vehicle, 'total_event': total_event, 'today_events': today_events, 'pending_events': pending_events, 'food_items': food_items, 'food_order': food_order, 'total_revenue': round(total_revenue, 2), 'today_revenue': round(today_revenue, 2), 'pending_payment': round(pending_payment, 2), 'currency_symbol': self.env.user.company_id.currency_id.symbol, 'currency_position': self.env.user.company_id.currency_id.position }