Files
pms/pms/models/pms_reservation.py
2021-02-05 13:28:37 +01:00

1653 lines
59 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
import time
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, float_compare, float_is_zero
_logger = logging.getLogger(__name__)
class PmsReservation(models.Model):
_name = "pms.reservation"
_description = "Reservation"
_inherit = ["mail.thread", "mail.activity.mixin", "portal.mixin"]
_order = "priority desc, create_date desc, write_date desc"
# TODO:
# consider near_to_checkin & pending_notifications to order
_check_company_auto = True
# Default Methods ang Gets
def _get_default_checkin(self):
folio = False
if "folio_id" in self._context:
folio = self.env["pms.folio"].search(
[("id", "=", self._context["folio_id"])]
)
if folio and folio.reservation_ids:
return folio.reservation_ids[0].checkin
else:
return fields.Date.today()
def _get_default_checkout(self):
folio = False
if "folio_id" in self._context:
folio = self.env["pms.folio"].search(
[("id", "=", self._context["folio_id"])]
)
if folio and folio.reservation_ids:
return folio.reservation_ids[0].checkout
else:
return fields.Date.today() + datetime.timedelta(1)
def _get_default_arrival_hour(self):
folio = False
# TODO: Change by property env variable (like company)
default_arrival_hour = self.env.user.pms_property_id.default_arrival_hour
if "folio_id" in self._context:
folio = self.env["pms.folio"].search(
[("id", "=", self._context["folio_id"])]
)
if folio and folio.reservation_ids:
return folio.reservation_ids[0].arrival_hour
else:
return default_arrival_hour
def _get_default_departure_hour(self):
folio = False
# TODO: Change by property env variable (like company)
default_departure_hour = self.env.user.pms_property_id.default_departure_hour
if "folio_id" in self._context:
folio = self.env["pms.folio"].search(
[("id", "=", self._context["folio_id"])]
)
if folio and folio.reservation_ids:
return folio.reservation_ids[0].departure_hour
else:
return default_departure_hour
def _get_default_segmentation(self):
folio = False
segmentation_ids = False
if "folio_id" in self._context:
folio = self.env["pms.folio"].search(
[("id", "=", self._context["folio_id"])]
)
if folio and folio.segmentation_ids:
segmentation_ids = folio.segmentation_ids
return segmentation_ids
# Fields declaration
name = fields.Text(
"Reservation Description",
compute="_compute_name",
store=True,
readonly=False,
# required=True,
)
priority = fields.Integer(compute="_compute_priority", store="True", index=True)
preferred_room_id = fields.Many2one(
"pms.room",
string="Room",
ondelete="restrict",
domain="[('id', 'in', allowed_room_ids)]",
copy=False,
)
allowed_room_ids = fields.Many2many(
"pms.room",
string="Allowed Rooms",
compute="_compute_allowed_room_ids",
)
folio_id = fields.Many2one(
"pms.folio",
string="Folio",
tracking=True,
ondelete="restrict",
copy=False,
)
board_service_room_id = fields.Many2one(
"pms.board.service.room.type",
string="Board Service",
compute="_compute_board_service_room_id",
store=True,
readonly=False,
)
room_type_id = fields.Many2one(
"pms.room.type",
string="Room Type",
tracking=True,
# required=True,
compute="_compute_room_type_id",
store=True,
readonly=False,
copy=False,
)
partner_id = fields.Many2one(
"res.partner",
tracking=True,
ondelete="restrict",
compute="_compute_partner_id",
store=True,
readonly=False,
)
agency_id = fields.Many2one(
related="folio_id.agency_id",
readonly=False,
store=True,
)
channel_type_id = fields.Many2one(
related="folio_id.channel_type_id",
store=True,
readonly=False,
)
partner_invoice_id = fields.Many2one(
"res.partner",
string="Invoice Address",
help="Invoice address for current reservation.",
compute="_compute_partner_invoice_id",
store=True,
readonly=False,
)
closure_reason_id = fields.Many2one(related="folio_id.closure_reason_id")
company_id = fields.Many2one(
related="folio_id.company_id", string="Company", store=True, readonly=True
)
pms_property_id = fields.Many2one(
"pms.property",
default=lambda self: self.env.user.get_active_property_ids()[0],
)
reservation_line_ids = fields.One2many(
"pms.reservation.line",
"reservation_id",
compute="_compute_reservation_line_ids",
store=True,
readonly=False,
copy=False,
)
service_ids = fields.One2many(
"pms.service",
"reservation_id",
compute="_compute_service_ids",
store=True,
readonly=False,
)
pricelist_id = fields.Many2one(
"product.pricelist",
string="Pricelist",
ondelete="restrict",
compute="_compute_pricelist_id",
store=True,
readonly=False,
)
commission_percent = fields.Float(
string="Commission percent (%)",
compute="_compute_commission_percent",
store=True,
readonly=False,
)
commission_amount = fields.Float(
string="Commission amount",
compute="_compute_commission_amount",
store=True,
)
# TODO: Warning Mens to update pricelist
checkin_partner_ids = fields.One2many(
"pms.checkin.partner",
"reservation_id",
compute="_compute_checkin_partner_ids",
store=True,
readonly=False,
copy=False,
)
count_pending_arrival = fields.Integer(
"Pending Arrival",
compute="_compute_count_pending_arrival",
store=True,
)
checkins_ratio = fields.Integer(
string="Pending Arrival Ratio",
compute="_compute_checkins_ratio",
)
pending_checkin_data = fields.Integer(
"Checkin Data",
compute="_compute_pending_checkin_data",
store=True,
)
ratio_checkin_data = fields.Integer(
string="Pending Checkin Data",
compute="_compute_ratio_checkin_data",
)
ready_for_checkin = fields.Boolean(compute="_compute_ready_for_checkin")
left_for_checkin = fields.Boolean(
compute="_compute_left_for_checkin", search="_search_left_for_checkin"
)
left_for_checkout = fields.Boolean(
compute="_compute_left_for_checkout", search="_search_left_for_checkout"
)
left_for_cancel = fields.Boolean(
compute="_compute_left_for_cancel", search="_search_left_for_cancel"
)
checkin_today = fields.Boolean(
compute="_compute_checkin_today", search="_search_checkin_today"
)
departure_today = fields.Boolean(
compute="_compute_departure_today", search="_search_departure_today"
)
segmentation_ids = fields.Many2many(
"res.partner.category",
string="Segmentation",
ondelete="restrict",
default=_get_default_segmentation,
)
currency_id = fields.Many2one(
"res.currency",
depends=["pricelist_id"],
store=True,
readonly=True,
)
tax_ids = fields.Many2many(
"account.tax",
string="Taxes",
compute="_compute_tax_ids",
readonly="False",
store=True,
ondelete="restrict",
domain=["|", ("active", "=", False), ("active", "=", True)],
)
move_line_ids = fields.Many2many(
"account.move.line",
"reservation_move_rel",
"reservation_id",
"move_line_id",
string="Invoice Lines",
copy=False,
)
localizator = fields.Char(
string="Localizator",
compute="_compute_localizator",
store=True,
)
adults = fields.Integer(
"Adults",
tracking=True,
help="List of adults there in guest list. ",
compute="_compute_adults",
store=True,
readonly=False,
)
children_occupying = fields.Integer(
string="Children occupying",
)
children = fields.Integer(
"Children",
readonly=False,
tracking=True,
help="Number of children there in guest list.",
)
to_assign = fields.Boolean(
string="To Assign",
tracking=True,
default=True,
)
state = fields.Selection(
[
("draft", "Pre-reservation"),
("confirm", "Pending arrival"),
("onboard", "On Board"),
("done", "Out"),
("cancelled", "Cancelled"),
("no_show", "No Show"),
("no_checkout", "No Checkout"),
],
string="Status",
default=lambda *a: "draft",
copy=False,
index=True,
tracking=True,
readonly=True,
)
reservation_type = fields.Selection(
related="folio_id.reservation_type", default=lambda *a: "normal"
)
splitted = fields.Boolean(
"Splitted",
compute="_compute_splitted",
store=True,
)
rooms = fields.Char(
string="Room/s",
compute="_compute_rooms",
store=True,
tracking=True,
)
credit_card_details = fields.Text(related="folio_id.credit_card_details")
cancelled_reason = fields.Selection(
[("late", "Late"), ("intime", "In time"), ("noshow", "No Show")],
string="Cause of cancelled",
tracking=True,
copy=False,
)
out_service_description = fields.Text("Cause of out of service")
checkin = fields.Date(
"Check In", required=True, default=_get_default_checkin, copy=False
)
checkout = fields.Date(
"Check Out", required=True, default=_get_default_checkout, copy=False
)
arrival_hour = fields.Char(
"Arrival Hour",
default=_get_default_arrival_hour,
help="Default Arrival Hour (HH:MM)",
)
departure_hour = fields.Char(
"Departure Hour",
default=_get_default_departure_hour,
help="Default Departure Hour (HH:MM)",
)
checkin_datetime = fields.Datetime(
"Exact Arrival",
compute="_compute_checkin_datetime",
)
checkout_datetime = fields.Datetime(
"Exact Departure",
compute="_compute_checkout_datetime",
)
checkin_partner_count = fields.Integer(
"Checkin counter", compute="_compute_checkin_partner_count"
)
checkin_partner_pending_count = fields.Integer(
"Checkin Pending Num",
compute="_compute_checkin_partner_count",
search="_search_checkin_partner_pending",
)
overbooking = fields.Boolean(
"Is Overbooking",
default=False,
copy=False,
)
reselling = fields.Boolean(
"Is Reselling",
default=False,
copy=False,
)
nights = fields.Integer("Nights", compute="_compute_nights", store=True)
origin = fields.Char("Origin", compute="_compute_origin", store=True)
detail_origin = fields.Char(
"Detail Origin", compute="_compute_detail_origin", store=True
)
folio_pending_amount = fields.Monetary(related="folio_id.pending_amount")
folio_payment_state = fields.Selection(
related="folio_id.payment_state",
string="Payment State",
store=True,
)
shared_folio = fields.Boolean(compute="_compute_shared")
# Used to notify is the reservation folio has other reservations/services
email = fields.Char("E-mail", related="partner_id.email")
mobile = fields.Char("Mobile", related="partner_id.mobile")
phone = fields.Char("Phone", related="partner_id.phone")
partner_internal_comment = fields.Text(
string="Internal Partner Notes", related="partner_id.comment"
)
requests = fields.Text(string="Partner Requests")
folio_internal_comment = fields.Text(
string="Internal Folio Notes",
related="folio_id.internal_comment",
)
preconfirm = fields.Boolean("Auto confirm to Save", default=True)
invoice_status = fields.Selection(
[
("upselling", "Upselling Opportunity"),
("invoiced", "Fully Invoiced"),
("to invoice", "To Invoice"),
("no", "Nothing to Invoice"),
],
string="Invoice Status",
compute="_compute_invoice_status",
store=True,
readonly=True,
default="no",
)
qty_to_invoice = fields.Float(
compute="_compute_qty_to_invoice",
string="To Invoice Quantity",
store=True,
readonly=True,
digits=("Product Unit of Measure"),
)
qty_invoiced = fields.Float(
compute="_compute_qty_invoiced",
string="Invoiced Quantity",
store=True,
readonly=True,
digits=("Product Unit of Measure"),
)
untaxed_amount_invoiced = fields.Monetary(
"Untaxed Invoiced Amount",
compute="_compute_untaxed_amount_invoiced",
compute_sudo=True,
store=True,
)
untaxed_amount_to_invoice = fields.Monetary(
"Untaxed Amount To Invoice",
compute="_compute_untaxed_amount_to_invoice",
compute_sudo=True,
store=True,
)
analytic_tag_ids = fields.Many2many(
"account.analytic.tag",
string="Analytic Tags",
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
)
analytic_line_ids = fields.One2many(
"account.analytic.line", "so_line", string="Analytic lines"
)
price_subtotal = fields.Monetary(
string="Subtotal",
readonly=True,
store=True,
compute="_compute_amount_reservation",
)
price_total = fields.Monetary(
string="Total",
readonly=True,
store=True,
compute="_compute_amount_reservation",
)
price_tax = fields.Float(
string="Taxes Amount",
readonly=True,
store=True,
compute="_compute_amount_reservation",
)
price_services = fields.Monetary(
string="Services Total",
readonly=True,
store=True,
compute="_compute_amount_room_services",
)
price_room_services_set = fields.Monetary(
string="Room Services Total",
readonly=True,
store=True,
compute="_compute_amount_set",
)
discount = fields.Float(
string="Discount (€)",
digits=("Discount"),
compute="_compute_discount",
readonly=False,
store=True,
)
date_order = fields.Date(
compute="_compute_pms_creation_date",
store=True,
readonly=False,
)
# Compute and Search methods
def _compute_pms_creation_date(self):
for record in self:
record.date_order = datetime.datetime.today()
@api.depends("checkin", "checkout", "room_type_id")
def _compute_name(self):
for reservation in self:
if (
reservation.room_type_id
and reservation.checkin
and reservation.checkout
):
checkin_str = reservation.checkin.strftime(DEFAULT_SERVER_DATE_FORMAT)
checkout_str = reservation.checkout.strftime(DEFAULT_SERVER_DATE_FORMAT)
reservation.name = (
reservation.room_type_id.name
+ ": "
+ checkin_str
+ " - "
+ checkout_str
)
else:
reservation.name = "/"
@api.depends("checkin")
def _compute_priority(self):
for record in self:
record.priority = 0
# we can give weights for each condition
if not record.to_assign:
record.priority += 1
if not record.left_for_checkin:
record.priority += 10
if record.left_for_checkout:
record.priority += 100
if record.state == "onboard" and record.folio_pending_amount > 0:
record.priority += 1000
@api.depends("pricelist_id", "room_type_id")
def _compute_board_service_room_id(self):
for reservation in self:
if reservation.pricelist_id and reservation.room_type_id:
board_service_default = self.env["pms.board.service.room.type"].search(
[
"&",
"&",
("pms_room_type_id", "=", reservation.room_type_id.id),
("by_default", "=", True),
"|",
("pricelist_id", "=", reservation.pricelist_id.id),
("pricelist_id", "=", False),
]
)
if len(board_service_default) > 1:
reservation.board_service_room_id = board_service_default.filtered(
lambda b: b.pricelist_id == reservation.pricelist_id
)
else:
reservation.board_service_room_id = (
board_service_default.id if board_service_default else False
)
elif not reservation.board_service_room_id:
reservation.board_service_room_id = False
@api.depends("preferred_room_id")
def _compute_room_type_id(self):
for reservation in self:
if reservation.preferred_room_id and not reservation.room_type_id:
reservation.room_type_id = reservation.preferred_room_id.room_type_id.id
elif not reservation.room_type_id:
reservation.room_type_id = False
@api.depends("checkin", "arrival_hour")
def _compute_checkin_datetime(self):
for reservation in self:
checkin_hour = int(reservation.arrival_hour[0:2])
checkin_minut = int(reservation.arrival_hour[3:5])
checkin_time = datetime.time(checkin_hour, checkin_minut)
checkin_datetime = datetime.datetime.combine(
reservation.checkin, checkin_time
)
reservation.checkin_datetime = (
reservation.pms_property_id.date_property_timezone(checkin_datetime)
)
@api.depends("checkout", "departure_hour")
def _compute_checkout_datetime(self):
for reservation in self:
checkout_hour = int(reservation.departure_hour[0:2])
checkout_minut = int(reservation.departure_hour[3:5])
checkout_time = datetime.time(checkout_hour, checkout_minut)
checkout_datetime = datetime.datetime.combine(
reservation.checkout, checkout_time
)
reservation.checkout_datetime = (
reservation.pms_property_id.date_property_timezone(checkout_datetime)
)
@api.depends(
"reservation_line_ids.date",
"overbooking",
"state",
"preferred_room_id",
"pricelist_id",
)
def _compute_allowed_room_ids(self):
for reservation in self:
if reservation.checkin and reservation.checkout:
if reservation.overbooking or reservation.state in ("cancelled"):
reservation.allowed_room_ids = self.env["pms.room"].search(
[("active", "=", True)]
)
return
rooms_available = self.env[
"pms.room.type.availability.plan"
].rooms_available(
checkin=reservation.checkin,
checkout=reservation.checkout,
room_type_id=False, # Allow chosen any available room
current_lines=reservation.reservation_line_ids.ids,
pricelist=reservation.pricelist_id.id,
)
reservation.allowed_room_ids = rooms_available
@api.depends("reservation_type", "agency_id", "folio_id")
def _compute_partner_id(self):
for reservation in self:
if reservation.reservation_type == "out":
reservation.partner_id = reservation.pms_property_id.partner_id.id
elif not reservation.partner_id:
if reservation.folio_id:
reservation.partner_id = reservation.folio_id.partner_id
elif reservation.agency_id:
reservation.partner_id = reservation.agency_id
else:
reservation.partner_id = False
@api.depends("partner_id")
def _compute_partner_invoice_id(self):
for reservation in self:
if reservation.folio_id and reservation.folio_id.partner_id:
addr = reservation.folio_id.partner_id.address_get(["invoice"])
else:
addr = reservation.partner_id.address_get(["invoice"])
reservation.partner_invoice_id = addr["invoice"]
@api.depends("checkin", "checkout")
def _compute_reservation_line_ids(self):
for reservation in self:
cmds = []
days_diff = (reservation.checkout - reservation.checkin).days
for i in range(0, days_diff):
idate = reservation.checkin + datetime.timedelta(days=i)
old_line = reservation.reservation_line_ids.filtered(
lambda r: r.date == idate
)
if not old_line:
cmds.append(
(
0,
False,
{"date": idate},
)
)
reservation.reservation_line_ids -= (
reservation.reservation_line_ids.filtered_domain(
[
"|",
("date", ">=", reservation.checkout),
("date", "<", reservation.checkin),
]
)
)
reservation.reservation_line_ids = cmds
@api.depends("board_service_room_id")
def _compute_service_ids(self):
for reservation in self:
board_services = []
old_board_lines = reservation.service_ids.filtered_domain(
[
("is_board_service", "=", True),
]
)
if reservation.board_service_room_id:
board = self.env["pms.board.service.room.type"].browse(
reservation.board_service_room_id.id
)
for line in board.board_service_line_ids:
res = {
"product_id": line.product_id.id,
"is_board_service": True,
"folio_id": reservation.folio_id.id,
"reservation_id": reservation.id,
}
board_services.append((0, False, res))
reservation.service_ids -= old_board_lines
reservation.service_ids = board_services
@api.depends("partner_id")
def _compute_pricelist_id(self):
for reservation in self:
# TODO: Review logic pricelist by partner
# and by allowed channel pricelist_ids
if reservation.folio_id:
pricelist_id = reservation.folio_id.pricelist_id.id
else:
pricelist_id = (
reservation.partner_id.property_product_pricelist
and reservation.partner_id.property_product_pricelist.id
or self.env.user.pms_property_id.default_pricelist_id.id
)
if reservation.pricelist_id.id != pricelist_id:
# TODO: Warning change de pricelist?
reservation.pricelist_id = pricelist_id
@api.depends("adults")
def _compute_checkin_partner_ids(self):
for reservation in self:
assigned_checkins = reservation.checkin_partner_ids.filtered(
lambda c: c.state in ("precheckin", "onboard", "done")
)
unassigned_checkins = reservation.checkin_partner_ids.filtered(
lambda c: c.state == "draft"
)
leftover_unassigneds_count = (
len(assigned_checkins) + len(unassigned_checkins) - reservation.adults
)
if len(assigned_checkins) > reservation.adults:
raise UserError(
_("Remove some of the leftover assigned checkins first")
)
elif leftover_unassigneds_count > 0:
for i in range(0, leftover_unassigneds_count):
unassigned_checkins[i].sudo().unlink()
elif reservation.adults > len(reservation.checkin_partner_ids):
checkins_lst = []
count_new_checkins = reservation.adults - len(
reservation.checkin_partner_ids
)
for _i in range(0, count_new_checkins):
checkins_lst.append(
(
0,
False,
{
"reservation_id": reservation.id,
},
)
)
reservation.with_context(
{"auto_create_checkin": True}
).checkin_partner_ids = checkins_lst
@api.depends("checkin_partner_ids", "checkin_partner_ids.state")
def _compute_count_pending_arrival(self):
for reservation in self:
reservation.count_pending_arrival = len(
reservation.checkin_partner_ids.filtered(
lambda c: c.state in ("draft", "precheckin")
)
)
@api.depends("count_pending_arrival")
def _compute_checkins_ratio(self):
self.checkins_ratio = 0
for reservation in self.filtered(lambda r: r.adults > 0):
reservation.checkins_ratio = (
(reservation.adults - reservation.count_pending_arrival)
* 100
/ reservation.adults
)
@api.depends("checkin_partner_ids", "checkin_partner_ids.state")
def _compute_pending_checkin_data(self):
for reservation in self:
reservation.pending_checkin_data = len(
reservation.checkin_partner_ids.filtered(lambda c: c.state == "draft")
)
@api.depends("pending_checkin_data")
def _compute_ratio_checkin_data(self):
self.ratio_checkin_data = 0
for reservation in self.filtered(lambda r: r.adults > 0):
reservation.ratio_checkin_data = (
(reservation.adults - reservation.pending_checkin_data)
* 100
/ reservation.adults
)
def _compute_left_for_checkin(self):
# Reservations still pending entry today
for record in self:
record.left_for_checkin = (
True
if (
record.state in ["draft", "confirm", "no_show"]
and record.checkin <= fields.Date.today()
)
else False
)
def _compute_left_for_checkout(self):
# Reservations still pending checkout today
for record in self:
record.left_for_checkout = (
True
if (
record.state in ["onboard", "no_checkout"]
and record.checkout >= fields.Date.today()
)
else False
)
def _compute_left_for_cancel(self):
# Reservations can be cancelled
for record in self:
record.left_for_cancel = (
True
if (record.state not in ["cancelled", "done", "no_checkout"])
else False
)
def _search_left_for_checkin(self, operator, value):
if operator not in ("=",):
raise UserError(
_("Invalid domain operator %s for left of checkin", operator)
)
if value not in (True,):
raise UserError(
_("Invalid domain right operand %s for left of checkin", value)
)
today = fields.Date.context_today(self)
return [
("state", "in", ("draft", "confirm", "no_show")),
("checkin", "<=", today),
]
def _search_left_for_checkout(self, operator, value):
if operator not in ("=",):
raise UserError(
_("Invalid domain operator %s for left of checkout", operator)
)
if value not in (True,):
raise UserError(
_("Invalid domain right operand %s for left of checkout", value)
)
today = fields.Date.context_today(self)
return [
("state", "in", ("onboard", "no_checkout")),
("checkout", ">=", today),
]
def _search_left_for_cancel(self, operator, value):
if operator not in ("=",):
raise UserError(
_("Invalid domain operator %s for left of cancel", operator)
)
if value not in (True,):
raise UserError(
_("Invalid domain right operand %s for left of cancel", value)
)
return [
("state", "not in", ("cancelled", "done", "no_checkout")),
]
def _compute_ready_for_checkin(self):
# Reservations with hosts data enought to checkin
for record in self:
record.ready_for_checkin = (
record.left_for_checkin
and len(
record.checkin_partner_ids.filtered(
lambda c: c.state == "precheckin"
)
)
>= 1
)
def _compute_checkin_today(self):
for record in self:
record.checkin_today = (
True if record.checkin == fields.Date.today() else False
)
# REVIEW: Late checkin?? (next day)
def _search_checkin_today(self, operator, value):
if operator not in ("=", "!="):
raise UserError(_("Invalid domain operator %s", operator))
if value not in (False, True):
raise UserError(_("Invalid domain right operand %s", value))
today = fields.Date.context_today(self)
return [("checkin", operator, today)]
def _compute_departure_today(self):
for record in self:
record.departure_today = (
True if record.checkout == fields.Date.today() else False
)
def _search_departure_today(self, operator, value):
if operator not in ("=", "!="):
raise UserError(_("Invalid domain operator %s", operator))
if value not in (False, True):
raise UserError(_("Invalid domain right operand %s", value))
searching_for_true = (operator == "=" and value) or (
operator == "!=" and not value
)
today = fields.Date.context_today(self)
return [("checkout", searching_for_true, today)]
@api.depends("agency_id")
def _compute_commission_percent(self):
for reservation in self:
if reservation.agency_id:
reservation.commission_percent = (
reservation.agency_id.default_commission
)
else:
reservation.commission_percent = 0
@api.depends("commission_percent", "price_total")
def _compute_commission_amount(self):
for reservation in self:
if reservation.commission_percent > 0:
reservation.commission_amount = (
reservation.price_total * reservation.commission_percent
)
else:
reservation.commission_amount = 0
# REVIEW: Dont run with set room_type_id -> room_id(compute)-> No set adults¿?
@api.depends("preferred_room_id")
def _compute_adults(self):
for reservation in self:
if reservation.preferred_room_id:
if reservation.adults == 0:
reservation.adults = reservation.preferred_room_id.capacity
elif not reservation.adults:
reservation.adults = 0
@api.depends("reservation_line_ids", "reservation_line_ids.room_id")
def _compute_splitted(self):
for reservation in self:
room_ids = reservation.reservation_line_ids.mapped("room_id.id")
if len(room_ids) > 1:
reservation.splitted = True
reservation.preferred_room_id = False
else:
reservation.splitted = False
if room_ids:
reservation.preferred_room_id = room_ids[0]
@api.depends("state", "qty_to_invoice", "qty_invoiced")
def _compute_invoice_status(self):
"""
Compute the invoice status of a Reservation. Possible statuses:
- no: if the Folio is not in status 'sale' or 'done', we consider
that there is nothing to invoice. This is also hte default value
if the conditions of no other status is met.
- to invoice: we refer to the quantity to invoice of the line.
Refer to method `_compute_get_to_invoice_qty()` for more information
on how this quantity is calculated.
- invoiced: the quantity invoiced is larger or equal to the
quantity ordered.
"""
precision = self.env["decimal.precision"].precision_get(
"Product Unit of Measure"
)
for line in self:
if line.state == "draft":
line.invoice_status = "no"
elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
line.invoice_status = "to invoice"
elif (
float_compare(
line.qty_invoiced,
len(line.reservation_line_ids),
precision_digits=precision,
)
>= 0
):
line.invoice_status = "invoiced"
else:
line.invoice_status = "no"
@api.depends("qty_invoiced", "nights", "folio_id.state")
def _compute_qty_to_invoice(self):
"""
Compute the quantity to invoice. The quantity to invoice is
calculated from the nights quantity.
"""
for line in self:
if line.folio_id.state not in ["draft"]:
line.qty_to_invoice = len(line.reservation_line_ids) - line.qty_invoiced
else:
line.qty_to_invoice = 0
@api.depends(
"move_line_ids.move_id.state",
"move_line_ids.quantity",
"untaxed_amount_to_invoice",
)
def _compute_qty_invoiced(self):
"""
Compute the quantity invoiced. If case of a refund, the quantity
invoiced is decreased. We must check day per day and sum or
decreased on 1 unit per invoice_line
"""
for record in self:
qty_invoiced = 0.0
for line in record.reservation_line_ids:
invoice_lines = line.move_line_ids.filtered(
lambda r: r.move_id.state != "cancel"
)
qty_invoiced += len(
invoice_lines.filtered(
lambda r: r.move_id.move_type == "out_invoice"
)
) - len(
invoice_lines.filtered(
lambda r: r.move_id.move_type == "out_refund"
)
)
record.qty_invoiced = qty_invoiced
@api.depends(
"move_line_ids",
"move_line_ids.price_total",
"move_line_ids.move_id.state",
"move_line_ids.move_id.move_type",
)
def _compute_untaxed_amount_invoiced(self):
"""Compute the untaxed amount already invoiced from the reservation,
taking the refund attached
the reservation into account. This amount is computed as
SUM(inv_line.price_subtotal) - SUM(ref_line.price_subtotal)
where
`inv_line` is a customer invoice line linked to the reservation
`ref_line` is a customer credit note (refund)
line linked to the reservation
"""
for line in self:
amount_invoiced = 0.0
for invoice_line in line.move_line_ids:
if invoice_line.move_id.state == "posted":
invoice_date = (
invoice_line.move_id.invoice_date or fields.Date.today()
)
if invoice_line.move_id.move_type == "out_invoice":
amount_invoiced += invoice_line.currency_id._convert(
invoice_line.price_subtotal,
line.currency_id,
line.company_id,
invoice_date,
)
elif invoice_line.move_id.move_type == "out_refund":
amount_invoiced -= invoice_line.currency_id._convert(
invoice_line.price_subtotal,
line.currency_id,
line.company_id,
invoice_date,
)
line.untaxed_amount_invoiced = amount_invoiced
@api.depends(
"state", "discount", "price_total", "room_type_id", "untaxed_amount_invoiced"
)
def _compute_untaxed_amount_to_invoice(self):
"""Total of remaining amount to invoice on the reservation (taxes excl.) as
total_sol - amount already invoiced
Note: Draft invoice are ignored on purpose, the 'to invoice' amount should
come only from the reservation.
"""
for line in self:
amount_to_invoice = 0.0
if line.state not in ["draft"]:
price_subtotal = 0.0
price_subtotal = line.price_subtotal
if len(line.tax_ids.filtered(lambda tax: tax.price_include)) > 0:
# As included taxes are not excluded from the computed subtotal,
# `compute_all()` method
# has to be called to retrieve the subtotal without them.
price_subtotal = line.tax_ids.compute_all(
price_subtotal,
currency=line.currency_id,
quantity=line.nights,
product=line.room_type_id.product_id,
)["total_excluded"]
if any(
line.move_line_ids.mapped(lambda i: i.discount != line.discount)
):
# In case of re-invoicing with different discount we
# try to calculate manually the
# remaining amount to invoice
amount = 0
for move in line.move_line_ids:
if (
len(move.tax_ids.filtered(lambda tax: tax.price_include))
> 0
):
amount += move.tax_ids.compute_all(
move.currency_id._convert(
move.price_unit,
line.currency_id,
line.company_id,
move.date or fields.Date.today(),
round=False,
)
* move.quantity
)["total_excluded"]
else:
amount += (
move.currency_id._convert(
move.price_unit,
line.currency_id,
line.company_id,
move.date or fields.Date.today(),
round=False,
)
* move.quantity
)
amount_to_invoice = max(price_subtotal - amount, 0)
else:
amount_to_invoice = price_subtotal - line.untaxed_amount_invoiced
line.untaxed_amount_to_invoice = amount_to_invoice
@api.depends("reservation_line_ids")
def _compute_nights(self):
for res in self:
res.nights = len(res.reservation_line_ids)
@api.depends("folio_id", "checkin", "checkout")
def _compute_localizator(self):
# TODO: Compute localizator by reservation
for record in self:
record.localizator = fields.date.today()
@api.depends("service_ids.price_total")
def _compute_amount_room_services(self):
for record in self:
record.price_services = sum(record.mapped("service_ids.price_total"))
@api.depends("price_services", "price_total")
def _compute_amount_set(self):
for record in self:
record.price_room_services_set = record.price_services + record.price_total
@api.depends(
"reservation_line_ids.discount", "reservation_line_ids.cancel_discount"
)
def _compute_discount(self):
for record in self:
discount = 0
for line in record.reservation_line_ids:
first_discount = line.price * ((line.discount or 0.0) * 0.01)
price = line.price - first_discount
cancel_discount = price * ((line.cancel_discount or 0.0) * 0.01)
discount += first_discount + cancel_discount
record.discount = discount
@api.depends("reservation_line_ids.price", "discount", "tax_ids")
def _compute_amount_reservation(self):
"""
Compute the amounts of the reservation.
"""
for record in self:
amount_room = sum(record.reservation_line_ids.mapped("price"))
if amount_room > 0:
product = record.room_type_id.product_id
price = amount_room - record.discount
taxes = record.tax_ids.compute_all(
price, record.currency_id, 1, product=product
)
record.update(
{
"price_tax": sum(
t.get("amount", 0.0) for t in taxes.get("taxes", [])
),
"price_total": taxes["total_included"],
"price_subtotal": taxes["total_excluded"],
}
)
else:
record.update(
{
"price_tax": 0,
"price_total": 0,
"price_subtotal": 0,
}
)
# TODO: Use default values on checkin /checkout is empty
@api.constrains(
"checkin", "checkout", "state", "preferred_room_id", "overbooking", "reselling"
)
def check_dates(self):
"""
1.-When date_order is less then checkin date or
Checkout date should be greater than the checkin date.
3.-Check the reservation dates are not occuped
"""
for record in self:
if fields.Date.from_string(record.checkin) >= fields.Date.from_string(
record.checkout
):
raise ValidationError(
_(
"Room line Check In Date Should be \
less than the Check Out Date!"
)
)
@api.constrains("checkin_partner_ids", "adults")
def _max_checkin_partner_ids(self):
for record in self:
if len(record.checkin_partner_ids) > record.adults:
raise models.ValidationError(
_("The room already is completed (%s)", record.name)
)
@api.constrains("adults")
def _check_adults(self):
for record in self:
extra_bed = record.service_ids.filtered(
lambda r: r.product_id.is_extra_bed is True
)
for room in record.reservation_line_ids.room_id:
if record.adults + record.children_occupying > room.get_capacity(
len(extra_bed)
):
raise ValidationError(
_(
"Persons can't be higher than room capacity (%s)",
record.name,
)
)
@api.constrains("state")
def _check_onboard_reservation(self):
for record in self:
if (
not record.checkin_partner_ids.filtered(lambda c: c.state == "onboard")
and record.state == "onboard"
):
raise ValidationError(
_("No person from reserve %s has arrived", record.name)
)
@api.constrains("arrival_hour")
def _check_arrival_hour(self):
for record in self:
try:
time.strptime(record.arrival_hour, "%H:%M")
return True
except ValueError:
raise ValidationError(
_("Format Arrival Hour (HH:MM) Error: %s", record.arrival_hour)
)
@api.constrains("departure_hour")
def _check_departure_hour(self):
for record in self:
try:
time.strptime(record.departure_hour, "%H:%M")
return True
except ValueError:
raise ValidationError(
_("Format Departure Hour (HH:MM) Error: %s", record.departure_hour)
)
@api.constrains("agency_id")
def _no_agency_as_agency(self):
for record in self:
if record.agency_id and not record.agency_id.is_agency:
raise ValidationError(_("booking agency with wrong configuration: "))
# Action methods
def open_folio(self):
action = self.env.ref("pms.open_pms_folio1_form_tree_all").sudo().read()[0]
if self.folio_id:
action["views"] = [(self.env.ref("pms.pms_folio_view_form").id, "form")]
action["res_id"] = self.folio_id.id
else:
action = {"type": "ir.actions.act_window_close"}
return action
def open_reservation_form(self):
action = self.env.ref("pms.open_pms_reservation_form_tree_all").sudo().read()[0]
action["views"] = [(self.env.ref("pms.pms_reservation_view_form").id, "form")]
action["res_id"] = self.id
return action
def action_pay_folio(self):
self.ensure_one()
return self.folio_id.action_pay()
def open_reservation_wizard(self):
rooms_available = self.env["pms.room.type.availability.plan"].rooms_available(
checkin=self.checkin,
checkout=self.checkout,
current_lines=self.reservation_line_ids.ids,
pricelist=self.pricelist_id.id,
)
# REVIEW: check capacity room
return {
"view_type": "form",
"view_mode": "form",
"name": "Unify the reservation",
"res_model": "pms.reservation.wizard",
"target": "new",
"type": "ir.actions.act_window",
"context": {
"rooms_available": rooms_available.ids,
},
}
# ORM Overrides
@api.model
def name_search(self, name="", args=None, operator="ilike", limit=100):
if args is None:
args = []
if not (name == "" and operator == "ilike"):
args += [
"|",
("folio_id.name", operator, name),
("preferred_room_id.name", operator, name),
]
return super(PmsReservation, self).name_search(
name="", args=args, operator="ilike", limit=limit
)
def name_get(self):
result = []
for res in self:
name = u"{} ({})".format(
res.folio_id.name, res.rooms if res.rooms else "No room"
)
result.append((res.id, name))
return result
def copy_data(self, default=None):
rooms_available = self.env["pms.room.type.availability.plan"].rooms_available(
self.checkin,
self.checkout,
room_type_id=self.room_type_id.id,
pricelist=self.pricelist_id.id,
)
if self.preferred_room_id.id in rooms_available.ids:
default["preferred_room_id"] = self.preferred_room_id.id
if self.room_type_id.id in rooms_available.mapped("room_type_id.id"):
default["room_type_id"] = self.room_type_id.id
return super(PmsReservation, self).copy_data(default)
@api.model
def create(self, vals):
if "folio_id" in vals:
folio = self.env["pms.folio"].browse(vals["folio_id"])
elif "partner_id" in vals:
folio_vals = {
"partner_id": int(vals.get("partner_id")),
}
# Create the folio in case of need
# (To allow to create reservations direct)
folio = self.env["pms.folio"].create(folio_vals)
vals.update(
{
"folio_id": folio.id,
"reservation_type": vals.get("reservation_type"),
}
)
record = super(PmsReservation, self).create(vals)
if record.preconfirm:
record.confirm()
return record
# Business methods
def _compute_shared(self):
# Has this reservation more charges associates in folio?,
# Yes?, then, this is share folio ;)
for record in self:
if record.folio_id:
record.shared_folio = len(record.folio_id.reservation_ids) > 1 or any(
record.folio_id.service_ids.filtered(
lambda x: x.reservation_id.id != record.id
)
)
else:
record.shared_folio = False
def _autoassign(self):
self.ensure_one()
room_chosen = False
rooms_available = self.env["pms.room.type.availability.plan"].rooms_available(
checkin=self.checkin,
checkout=self.checkout,
room_type_id=self.room_type_id.id or False,
pricelist=self.pricelist_id.id,
)
if rooms_available:
room_chosen = rooms_available[0]
else:
# We can split reserve night on multi rooms
room_chosen = False
return room_chosen
@api.model
def autocheckout(self):
reservations = self.env["pms.reservation"].search(
[
("state", "not in", ["done", "cancelled"]),
("checkout", "<", fields.Date.today()),
]
)
for res in reservations:
res.action_reservation_checkout()
res_without_checkin = reservations.filtered(lambda r: r.state != "onboard")
for res in res_without_checkin:
msg = _("No checkin was made for this reservation")
res.message_post(subject=_("No Checkins!"), subtype="mt_comment", body=msg)
return True
def overbooking_button(self):
self.ensure_one()
self.overbooking = not self.overbooking
def generate_copy_values(self, checkin=False, checkout=False):
self.ensure_one()
return {
"name": self.name,
"adults": self.adults,
"children": self.children,
"checkin": checkin or self.checkin,
"checkout": checkout or self.checkout,
"folio_id": self.folio_id.id,
"state": self.state,
"overbooking": self.overbooking,
"reselling": self.reselling,
"price_total": self.price_total,
"price_tax": self.price_tax,
"price_subtotal": self.price_subtotal,
"splitted": self.splitted,
"room_type_id": self.room_type_id.id,
"preferred_room_id": self.preferred_room_id.id,
}
def confirm(self):
for record in self:
vals = {}
if record.checkin_partner_ids.filtered(lambda c: c.state == "onboard"):
vals.update({"state": "onboard"})
else:
vals.update({"state": "confirm"})
record.write(vals)
record.reservation_line_ids.update({"cancel_discount": 0})
if record.folio_id.state != "confirm":
record.folio_id.action_confirm()
return True
def action_cancel(self):
for record in self:
cancel_reason = (
"intime"
if self._context.get("no_penalty", False)
else record.compute_cancelation_reason()
)
if self._context.get("no_penalty", False):
_logger.info("Modified Reservation - No Penalty")
record.write({"state": "cancelled", "cancelled_reason": cancel_reason})
# record._compute_cancelled_discount()
record.folio_id._compute_amount()
def compute_cancelation_reason(self):
self.ensure_one()
pricelist = self.pricelist_id
if pricelist and pricelist.cancelation_rule_id:
tz_property = self.env.user.pms_property_id.tz
today = fields.Date.context_today(self.with_context(tz=tz_property))
days_diff = (
fields.Date.from_string(self.checkin) - fields.Date.from_string(today)
).days
if days_diff < 0:
return "noshow"
elif days_diff < pricelist.cancelation_rule_id.days_intime:
return "late"
else:
return "intime"
return False
def draft(self):
for record in self:
record.state = "draft"
record.reservation_line_ids.update({"cancel_discount": 0})
# INFO: This function is not in use and should include `dto` in the search
@api.model
def get_reservations_dates(self, dfrom, dto, room_type=False):
"""
@param self: The object pointer
@param dfrom: range date from
@param dto: range date to
@return: dictionary of lists with reservations (a hash of arrays!)
with the reservations dates between dfrom and dto
reservations_dates
{'2018-07-30': [pms.reservation(29,), pms.reservation(30,),
pms.reservation(31,)],
'2018-07-31': [pms.reservation(22,), pms.reservation(35,),
pms.reservation(36,)],
}
"""
domain = [("date", ">=", dfrom), ("date", "<", dto)]
lines = self.env["pms.reservation.line"].search(domain)
reservations_dates = {}
for record in lines:
reservations_dates.setdefault(record.date, []).append(
[record.reservation_id, record.reservation_id.room_type_id]
)
return reservations_dates
def _compute_checkin_partner_count(self):
for record in self:
if record.reservation_type != "out":
record.checkin_partner_count = len(record.checkin_partner_ids)
record.checkin_partner_pending_count = (
record.adults + record.children
) - len(record.checkin_partner_ids)
else:
record.checkin_partner_count = 0
record.checkin_partner_pending_count = 0
def _search_checkin_partner_pending(self, operator, value):
self.ensure_one()
recs = self.search([]).filtered(lambda x: x.checkin_partner_pending_count > 0)
return [("id", "in", [x.id for x in recs])] if recs else []
def action_reservation_checkout(self):
for record in self:
if record.state not in ("onboard", "no_checkout"):
raise UserError(_("This reservation cannot be check out"))
record.state = "done"
if record.checkin_partner_ids:
record.checkin_partner_ids.filtered(
lambda check: check.state == "onboard"
).action_done()
return True
def action_checks(self):
self.ensure_one()
tree_id = self.env.ref("pms.pms_checkin_partner_reservation_view_tree").id
return {
"name": _("Register Partners"),
"views": [[tree_id, "tree"]],
"res_model": "pms.checkin.partner",
"type": "ir.actions.act_window",
"context": {
"create": False,
"edit": True,
"popup": True,
},
"domain": [("reservation_id", "=", self.id), ("state", "=", "draft")],
"search_view_id": [
self.env.ref("pms.pms_checkin_partner_view_folio_search").id,
"search",
],
"target": "new",
}
def action_onboard(self):
self.ensure_one()
kanban_id = self.env.ref("pms.pms_checkin_partner_kanban_view").id
return {
"name": _("Register Checkins"),
"views": [[kanban_id, "kanban"]],
"res_model": "pms.checkin.partner",
"type": "ir.actions.act_window",
"context": {
"create": False,
"edit": True,
"popup": True,
},
"search_view_id": [
self.env.ref("pms.pms_checkin_partner_view_folio_search").id,
"search",
],
"domain": [("reservation_id", "=", self.id)],
"target": "new",
}
@api.model
def auto_no_show(self):
# No show when pass 1 day from checkin day
no_show_reservations = self.env["pms.reservation"].search(
[
("state", "in", ("draft", "confirm")),
("checkin", "<", fields.Date.today()),
]
)
no_show_reservations.state = "no_show"
@api.model
def auto_no_checkout(self):
# No checkout when pass checkout hour
reservations = self.env["pms.reservation"].search(
[
("state", "in", ("onboard",)),
("checkout", "=", fields.Datetime.today()),
]
)
for reservation in reservations:
if reservation.checkout_datetime <= fields.Datetime.now():
reservations.state = "no_checkout"
@api.depends("room_type_id")
def _compute_tax_ids(self):
for record in self:
record = record.with_company(record.company_id)
product = self.env["product.product"].browse(
record.room_type_id.product_id.id
)
record.tax_ids = product.taxes_id.filtered(
lambda t: t.company_id == record.env.company
)
@api.depends("reservation_line_ids", "reservation_line_ids.room_id")
def _compute_rooms(self):
self.rooms = False
for reservation in self:
if reservation.splitted:
reservation.rooms = ", ".join(
[r for r in reservation.reservation_line_ids.mapped("room_id.name")]
)
reservation.preferred_room_id = False
else:
reservation.rooms = reservation.preferred_room_id.name