Files
pms/pms/models/pms_reservation_line.py

513 lines
21 KiB
Python

# Copyright 2017-2018 Alexandre Díaz
# Copyright 2017 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import datetime
import logging
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class PmsReservationLine(models.Model):
_name = "pms.reservation.line"
_description = "Reservations by day"
_order = "date"
_check_company_auto = True
reservation_id = fields.Many2one(
string="Reservation",
help="It is the reservation in a reservation line",
required=True,
copy=False,
comodel_name="pms.reservation",
ondelete="cascade",
check_pms_properties=True,
)
room_id = fields.Many2one(
string="Room",
help="The room of a reservation. ",
readonly=False,
store=True,
compute="_compute_room_id",
comodel_name="pms.room",
ondelete="restrict",
check_pms_properties=True,
)
sale_line_ids = fields.Many2many(
string="Sales Lines",
readonly=True,
copy=False,
comodel_name="folio.sale.line",
check_pms_properties=True,
)
pms_property_id = fields.Many2one(
string="Property",
help="Property with access to the element;"
" if not set, all properties can access",
readonly=True,
store=True,
comodel_name="pms.property",
related="reservation_id.pms_property_id",
check_pms_properties=True,
)
date = fields.Date(
string="Date",
help="The date of the reservation in reservation line",
)
state = fields.Selection(
string="State",
help="State of the reservation line.",
related="reservation_id.state",
store=True,
)
price = fields.Float(
string="Price",
help="The price in a reservation line",
store=True,
readonly=False,
digits=("Product Price"),
compute="_compute_price",
)
cancel_discount = fields.Float(
string="Cancelation Discount (%)",
help="",
readonly=False,
default=0.0,
store=True,
digits=("Discount"),
compute="_compute_cancel_discount",
)
avail_id = fields.Many2one(
string="Availability Day",
help="",
store=True,
comodel_name="pms.availability",
ondelete="restrict",
compute="_compute_avail_id",
check_pms_properties=True,
)
discount = fields.Float(
string="Discount (%)",
help="",
default=0.0,
digits=("Discount"),
)
occupies_availability = fields.Boolean(
string="Occupies",
help="This record is taken into account to calculate availability",
store=True,
compute="_compute_occupies_availability",
)
overnight_room = fields.Boolean(
related="reservation_id.overnight_room",
store=True,
)
overbooking = fields.Boolean(
string="Overbooking",
help="Indicate if exists overbooking in the reservation line",
store=True,
readonly=False,
compute="_compute_overbooking",
)
sale_channel_id = fields.Many2one(
string="Sale Channel",
help="Sale Channel through which reservation line was created",
comodel_name="pms.sale.channel",
check_pms_properties=True,
)
_sql_constraints = [
(
"rule_availability",
"EXCLUDE (room_id WITH =, date WITH =) \
WHERE (occupies_availability = True)",
"Room Occupied",
),
]
def name_get(self):
result = []
for res in self:
date = fields.Date.from_string(res.date)
name = "{}/{}".format(date.day, date.month)
result.append((res.id, name))
return result
def _get_display_price(self, product):
if self.reservation_id.pricelist_id.discount_policy == "with_discount":
return product.with_context(
pricelist=self.reservation_id.pricelist_id.id
).price
product_context = dict(
self.env.context,
partner_id=self.reservation_id.partner_id.id,
date=self.date,
uom=product.uom_id.id,
)
final_price, rule_id = self.reservation_id.pricelist_id.with_context(
product_context
).get_product_price_rule(product, 1.0, self.reservation_id.partner_id)
base_price, currency = self.with_context(
product_context
)._get_real_price_currency(
product, rule_id, 1, product.uom_id, self.reservation_id.pricelist_id.id
)
if currency != self.reservation_id.pricelist_id.currency_id:
base_price = currency._convert(
base_price,
self.reservation_id.pricelist_id.currency_id,
self.reservation_id.company_id or self.env.company,
fields.Date.today(),
)
# negative discounts (= surcharge) are included in the display price
return max(base_price, final_price)
@api.depends("reservation_id.room_type_id", "reservation_id.preferred_room_id")
def _compute_room_id(self):
for line in self.filtered("reservation_id.room_type_id").sorted(
key=lambda r: (r.reservation_id, r.date)
):
reservation = line.reservation_id
if (
reservation.preferred_room_id
and reservation.preferred_room_id != line.room_id
) or (
(reservation.preferred_room_id or reservation.room_type_id)
and not line.room_id
):
free_room_select = True if reservation.preferred_room_id else False
# we get the rooms available for the entire stay
# (real_avail if True if the reservation was created with
# specific room selected)
pms_property = line.pms_property_id
pms_property = pms_property.with_context(
checkin=reservation.checkin,
checkout=reservation.checkout,
room_type_id=reservation.room_type_id.id
if not free_room_select
else False,
current_lines=reservation.reservation_line_ids.ids,
pricelist_id=reservation.pricelist_id.id,
real_avail=free_room_select,
)
rooms_available = pms_property.free_room_ids
# Check if the room assigment is manual or automatic to set the
# to_assign value on reservation
manual_assigned = False
if (
free_room_select
and reservation.preferred_room_id.id
not in reservation.reservation_line_ids.room_id.ids
):
# This case is a preferred_room_id manually assigned
manual_assigned = True
# if there is availability for the entire stay
if rooms_available:
# Avoid that reservation._compute_splitted set the
# reservation like splitted in intermediate calculations
reservation = reservation.with_context(not_split=True)
# if the reservation has a preferred room
if reservation.preferred_room_id:
# if the preferred room is available
if reservation.preferred_room_id in rooms_available:
line.room_id = reservation.preferred_room_id
reservation.to_assign = (
False if manual_assigned else reservation.to_assign
)
# if the preferred room is NOT available
else:
if self.env.context.get("force_overbooking"):
reservation.overbooking = True
line.room_id = reservation.preferred_room_id
else:
raise ValidationError(
_("%s: No room available in %s <-> %s.")
% (
reservation.preferred_room_id.name,
reservation.checkin,
reservation.checkout,
)
)
# otherwise we assign the first of those
# available for the entire stay
else:
line.room_id = rooms_available[0]
# check that the reservation cannot be allocated even by dividing it
elif not self.env["pms.property"].splitted_availability(
checkin=reservation.checkin,
checkout=reservation.checkout,
room_type_id=reservation.room_type_id.id,
current_lines=line._origin.reservation_id.reservation_line_ids.ids,
pricelist=reservation.pricelist_id,
pms_property_id=line.pms_property_id.id,
):
if self.env.context.get("force_overbooking"):
reservation.overbooking = True
line.room_id = reservation.room_type_id.room_ids.filtered(
lambda r: r.pms_property_id == line.pms_property_id
)[0]
else:
raise ValidationError(
_("%s: No room type available")
% (reservation.room_type_id.name)
)
# the reservation can be allocated into several rooms
else:
rooms_ranking = dict()
# we go through the rooms of the type
for room in self.env["pms.room"].search(
[
("room_type_id", "=", reservation.room_type_id.id),
("pms_property_id", "=", reservation.pms_property_id.id),
]
):
# we iterate the dates from the date of the line to the checkout
for date_iterator in [
line.date + datetime.timedelta(days=x)
for x in range(0, (reservation.checkout - line.date).days)
]:
# if the room is already assigned for
# a date we go to the next room
ids = reservation.reservation_line_ids.ids
if (
self.env["pms.reservation.line"].search_count(
[
("date", "=", date_iterator),
("room_id", "=", room.id),
("id", "not in", ids),
("occupies_availability", "=", True),
]
)
> 0
):
break
# if the room is not assigned for a date we
# add it to the ranking / update its ranking
else:
rooms_ranking[room.id] = (
1
if room.id not in rooms_ranking
else rooms_ranking[room.id] + 1
)
if len(rooms_ranking) > 0:
# we get the best score in the ranking
best = max(rooms_ranking.values())
# we keep the rooms with the best ranking
bests = {
key: value
for (key, value) in rooms_ranking.items()
if value == best
}
# if there is a tie in the rankings
if len(bests) > 1:
# we get the line from last night
date_last_night = line.date + datetime.timedelta(days=-1)
line_past_night = self.env["pms.reservation.line"].search(
[
("date", "=", date_last_night),
("reservation_id", "=", reservation.id),
]
)
# if there is the night before and if the room
# from the night before is in the ranking
if line_past_night and line_past_night.room_id.id in bests:
line.room_id = line_past_night.room_id.id
# if the room from the night before is not in the ranking
# or there is no night before
else:
# At this point we set the room with the best ranking,
# no matter what it is
line.room_id = list(bests.keys())[0]
# if there is no tie in the rankings
else:
# At this point we set the room with the best ranking,
# no matter what it is
line.room_id = list(bests.keys())[0]
@api.depends(
"reservation_id",
"reservation_id.room_type_id",
"reservation_id.reservation_type",
"reservation_id.pms_property_id",
)
def _compute_price(self):
for line in self:
reservation = line.reservation_id
if (
not reservation.room_type_id
or not reservation.pricelist_id
or not reservation.pms_property_id
or reservation.reservation_type != "normal"
):
line.price = 0
elif not line.price or self._context.get("force_recompute"):
room_type_id = reservation.room_type_id.id
product = self.env["pms.room.type"].browse(room_type_id).product_id
partner = self.env["res.partner"].browse(reservation.partner_id.id)
product = product.with_context(
lang=partner.lang,
partner=partner.id,
quantity=1,
date=reservation.date_order,
consumption_date=line.date,
pricelist=reservation.pricelist_id.id,
uom=product.uom_id.id,
property=reservation.pms_property_id.id,
)
line.price = self.env["account.tax"]._fix_tax_included_price_company(
line._get_display_price(product),
product.taxes_id,
reservation.tax_ids,
reservation.company_id,
)
# TODO: Out of service 0 amount
@api.depends("reservation_id.state", "reservation_id.overbooking")
def _compute_occupies_availability(self):
for line in self:
if line.reservation_id.state == "cancel" or line.reservation_id.overbooking:
line.occupies_availability = False
else:
line.occupies_availability = True
# TODO: Refact method and allowed cancelled single days
@api.depends("reservation_id.cancelled_reason")
def _compute_cancel_discount(self):
for line in self:
line.cancel_discount = 0
reservation = line.reservation_id
pricelist = reservation.pricelist_id
if reservation.state == "cancel":
if (
reservation.cancelled_reason
and pricelist
and pricelist.cancelation_rule_id
):
checkin = fields.Date.from_string(reservation.checkin)
checkout = fields.Date.from_string(reservation.checkout)
days = abs((checkin - checkout).days)
rule = pricelist.cancelation_rule_id
discount = 0
if reservation.cancelled_reason == "late":
discount = 100 - rule.penalty_late
if rule.apply_on_late == "first":
days = 1
elif rule.apply_on_late == "days":
days = rule.days_late
elif reservation.cancelled_reason == "noshow":
discount = 100 - rule.penalty_noshow
if rule.apply_on_noshow == "first":
days = 1
elif rule.apply_on_noshow == "days":
days = rule.days_late - 1
elif reservation.cancelled_reason == "intime":
discount = 100
dates = []
for i in range(0, days):
dates.append(
fields.Date.from_string(
fields.Date.from_string(checkin)
+ datetime.timedelta(days=i)
)
)
reservation.reservation_line_ids.filtered(
lambda r: r.date in dates
).update({"cancel_discount": discount})
reservation.reservation_line_ids.filtered(
lambda r: r.date not in dates
).update({"cancel_discount": 100})
else:
reservation.reservation_line_ids.update({"cancel_discount": 0})
else:
reservation.reservation_line_ids.update({"cancel_discount": 0})
@api.depends("room_id", "pms_property_id", "date", "occupies_availability")
def _compute_avail_id(self):
for record in self:
if record.room_id.room_type_id and record.date and record.pms_property_id:
avail = self.env["pms.availability"].search(
[
("date", "=", record.date),
("room_type_id", "=", record.room_id.room_type_id.id),
("pms_property_id", "=", record.pms_property_id.id),
]
)
if avail:
record.avail_id = avail.id
else:
record.avail_id = self.env["pms.availability"].create(
{
"date": record.date,
"room_type_id": record.room_id.room_type_id.id,
"pms_property_id": record.pms_property_id.id,
}
)
else:
record.avail_id = False
@api.depends("reservation_id.overbooking")
def _compute_overbooking(self):
for record in self:
if record.reservation_id.overbooking:
real_avail = (
self.env["pms.availability"]
.search(
[
("room_type_id", "=", record.room_id.room_type_id.id),
("date", "=", record.date),
("pms_property_id", "=", record.pms_property_id.id),
]
)
.real_avail
)
if real_avail == 0:
record.overbooking = True
else:
record.overbooking = False
else:
record.overbooking = False
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for line in records:
reservation = line.reservation_id
# Set default channel
if not line.sale_channel_id:
line.sale_channel_id = reservation.sale_channel_origin_id.id
# Update quota
self.env["pms.availability.plan"].update_quota(
pricelist_id=reservation.pricelist_id.id,
room_type_id=reservation.room_type_id.id,
date=line.date,
pms_property_id=reservation.pms_property_id.id,
)
return records
# Constraints and onchanges
@api.constrains("date")
def constrains_duplicated_date(self):
for record in self:
duplicated = record.reservation_id.reservation_line_ids.filtered(
lambda r: r.date == record.date and r.id != record.id
)
if duplicated:
raise ValidationError(_("Duplicated reservation line date"))