mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
1090 lines
41 KiB
Python
1090 lines
41 KiB
Python
# Copyright 2019 Pablo Quesada
|
|
# Copyright 2019 Dario Lodeiros
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
|
|
import base64
|
|
import datetime
|
|
import time
|
|
|
|
import pytz
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import _, api, fields, models, modules
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.osv import expression
|
|
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
|
|
|
from odoo.addons.base.models.res_partner import _tz_get
|
|
|
|
|
|
def get_default_logo():
|
|
with open(
|
|
modules.get_module_resource("pms", "static/img", "property_logo.png"), "rb"
|
|
) as f:
|
|
return base64.b64encode(f.read())
|
|
|
|
|
|
class PmsProperty(models.Model):
|
|
_name = "pms.property"
|
|
_description = "Property"
|
|
_inherits = {"res.partner": "partner_id"}
|
|
_check_company_auto = True
|
|
|
|
partner_id = fields.Many2one(
|
|
string="Property",
|
|
help="Current property",
|
|
comodel_name="res.partner",
|
|
required=True,
|
|
index=True,
|
|
ondelete="restrict",
|
|
)
|
|
parent_id = fields.Many2one(
|
|
comodel_name="pms.property", string="Parent Property", index=True
|
|
)
|
|
child_ids = fields.One2many(
|
|
comodel_name="pms.property", inverse_name="parent_id", string="Child Properties"
|
|
)
|
|
pms_property_code = fields.Char(
|
|
string="Property Code",
|
|
help="Short name property",
|
|
)
|
|
company_id = fields.Many2one(
|
|
string="Company",
|
|
help="The company that owns or operates this property.",
|
|
comodel_name="res.company",
|
|
index=True,
|
|
required=True,
|
|
)
|
|
user_ids = fields.Many2many(
|
|
string="Accepted Users",
|
|
help="Field related to res.users. Allowed users on the property",
|
|
comodel_name="res.users",
|
|
relation="pms_property_users_rel",
|
|
column1="pms_property_id",
|
|
column2="user_id",
|
|
)
|
|
room_ids = fields.One2many(
|
|
string="Rooms",
|
|
help="Rooms that a property has.",
|
|
comodel_name="pms.room",
|
|
inverse_name="pms_property_id",
|
|
)
|
|
default_pricelist_id = fields.Many2one(
|
|
string="Product Pricelist",
|
|
help="The default pricelist used in this property.",
|
|
comodel_name="product.pricelist",
|
|
required=True,
|
|
index=True,
|
|
domain="[('is_pms_available', '=', True)]",
|
|
default=lambda self: self.env.ref("product.list0").id,
|
|
)
|
|
default_arrival_hour = fields.Char(
|
|
string="Arrival Hour", help="HH:mm Format", default="14:00"
|
|
)
|
|
default_departure_hour = fields.Char(
|
|
string="Departure Hour", help="HH:mm Format", default="12:00"
|
|
)
|
|
folio_sequence_id = fields.Many2one(
|
|
string="Folio Sequence",
|
|
help="The sequence that formed the name of the folio.",
|
|
check_company=True,
|
|
copy=False,
|
|
index=True,
|
|
comodel_name="ir.sequence",
|
|
)
|
|
checkin_sequence_id = fields.Many2one(
|
|
string="Checkin Sequence",
|
|
help="Field used to create the name of the checkin partner",
|
|
check_company=True,
|
|
copy=False,
|
|
index=True,
|
|
comodel_name="ir.sequence",
|
|
)
|
|
|
|
tz = fields.Selection(
|
|
string="Timezone",
|
|
help="This field is used to determine de timezone of the property.",
|
|
required=True,
|
|
default=lambda self: self.env.user.tz or "UTC",
|
|
selection=_tz_get,
|
|
)
|
|
|
|
cardex_warning = fields.Text(
|
|
string="Warning in Cardex",
|
|
default="Time to access rooms: 14: 00h. "
|
|
"Departure time: 12: 00h. If the accommodation "
|
|
"is not left at that time, the establishment will "
|
|
"charge a day's stay according to current rate that day",
|
|
help="Notice under the signature on the traveler's ticket.",
|
|
)
|
|
free_room_ids = fields.One2many(
|
|
string="Rooms available",
|
|
help="allows you to send different parameters in the context "
|
|
"(checkin(required), checkout(required), room_type_id, ubication_id, capacity, "
|
|
"amenity_ids and / or pricelist_id) and return rooms available",
|
|
comodel_name="pms.room",
|
|
compute="_compute_free_room_ids",
|
|
)
|
|
availability = fields.Integer(
|
|
string="Number of rooms available",
|
|
help="allows you to send different parameters in the context "
|
|
"(checkin(required), checkout(required), room_type_id, ubication_id, capacity,"
|
|
"amenity_ids and / or pricelist_id) check the availability for the hotel",
|
|
compute="_compute_availability",
|
|
)
|
|
|
|
mail_information = fields.Html(
|
|
string="Mail Information", help="Additional information of the mail"
|
|
)
|
|
|
|
privacy_policy = fields.Html(string="Privacy Policy", help="Mail privacy policy ")
|
|
|
|
property_confirmed_template = fields.Many2one(
|
|
string="Confirmation Email",
|
|
help="Confirmation email template",
|
|
comodel_name="mail.template",
|
|
)
|
|
|
|
property_modified_template = fields.Many2one(
|
|
string="Modification Email",
|
|
help="Modification email template",
|
|
comodel_name="mail.template",
|
|
)
|
|
|
|
property_exit_template = fields.Many2one(
|
|
string="Exit Email",
|
|
comodel_name="mail.template",
|
|
)
|
|
|
|
property_canceled_template = fields.Many2one(
|
|
string="Cancellation Email",
|
|
help="Cancellation email template",
|
|
comodel_name="mail.template",
|
|
)
|
|
|
|
is_confirmed_auto_mail = fields.Boolean(string="Auto Send Confirmation Mail")
|
|
is_modified_auto_mail = fields.Boolean(string="Auto Send Modification Mail")
|
|
is_exit_auto_mail = fields.Boolean(string="Auto Send Exit Mail")
|
|
is_canceled_auto_mail = fields.Boolean(string="Auto Send Cancellation Mail")
|
|
|
|
default_invoicing_policy = fields.Selection(
|
|
string="Default Invoicing Policy",
|
|
selection=[
|
|
("manual", "Manual"),
|
|
("checkout", "Checkout"),
|
|
("month_day", "Month Day Invoice"),
|
|
],
|
|
default="manual",
|
|
)
|
|
|
|
margin_days_autoinvoice = fields.Integer(
|
|
string="Margin Days",
|
|
help="Days from Checkout to generate the invoice",
|
|
)
|
|
|
|
invoicing_month_day = fields.Integer(
|
|
string="Invoicing Month Day",
|
|
help="The day of the month to invoice",
|
|
)
|
|
|
|
journal_simplified_invoice_id = fields.Many2one(
|
|
string="Simplified Invoice Journal",
|
|
comodel_name="account.journal",
|
|
domain=[
|
|
("type", "=", "sale"),
|
|
],
|
|
help="Journal used to create the simplified invoice",
|
|
check_company=True,
|
|
check_pms_properties=True,
|
|
)
|
|
|
|
journal_normal_invoice_id = fields.Many2one(
|
|
string="Normal Invoice Journal",
|
|
comodel_name="account.journal",
|
|
domain=[
|
|
("type", "=", "sale"),
|
|
("is_simplified_invoice", "=", False),
|
|
],
|
|
help="Journal used to create the normal invoice",
|
|
check_company=True,
|
|
check_pms_properties=True,
|
|
)
|
|
|
|
max_amount_simplified_invoice = fields.Float(
|
|
string="Max Amount Simplified Invoice",
|
|
help="Maximum amount to create the simplified invoice",
|
|
default=400.0,
|
|
)
|
|
avoid_simplified_max_amount_downpayment = fields.Boolean(
|
|
string="Downpayment Invoive without limit amount",
|
|
help="Avoid simplified invoice max amount downpayment",
|
|
default=True,
|
|
)
|
|
user_id = fields.Many2one(
|
|
string="Team Leader",
|
|
copy=False,
|
|
comodel_name="res.users",
|
|
ondelete="restrict",
|
|
tracking=True,
|
|
)
|
|
member_ids = fields.One2many(
|
|
string="Team Members",
|
|
comodel_name="pms.team.member",
|
|
inverse_name="pms_property_id",
|
|
copy=False,
|
|
)
|
|
logo = fields.Binary(
|
|
string="Image in checkin",
|
|
default=get_default_logo(),
|
|
)
|
|
|
|
color_option_config = fields.Selection(
|
|
string="Color Option Configuration",
|
|
help="Configuration of the color code for the planning.",
|
|
selection=[("simple", "Simple"), ("advanced", "Advanced")],
|
|
default="simple",
|
|
)
|
|
|
|
simple_out_color = fields.Char(
|
|
string="Reservations Outside",
|
|
help="Color for done reservations in the planning.",
|
|
default="rgba(94,208,236)",
|
|
)
|
|
|
|
simple_in_color = fields.Char(
|
|
string="Reservations Inside",
|
|
help="Color for onboard and departure_delayed reservations in the planning.",
|
|
default="rgba(0,146,183)",
|
|
)
|
|
|
|
simple_future_color = fields.Char(
|
|
string="Future Reservations",
|
|
help="Color for confirm, arrival_delayed and draft reservations in the planning.",
|
|
default="rgba(1,182,227)",
|
|
)
|
|
|
|
pre_reservation_color = fields.Char(
|
|
string="Pre-Reservation",
|
|
help="Color for draft reservations in the planning.",
|
|
default="rgba(162,70,128)",
|
|
)
|
|
|
|
confirmed_reservation_color = fields.Char(
|
|
string="Confirmed Reservation",
|
|
default="rgba(1,182,227)",
|
|
help="Color for confirm reservations in the planning.",
|
|
)
|
|
|
|
paid_reservation_color = fields.Char(
|
|
string="Paid Reservation",
|
|
help="Color for done paid reservations in the planning.",
|
|
default="rgba(126,126,126)",
|
|
)
|
|
|
|
on_board_reservation_color = fields.Char(
|
|
string="Checkin",
|
|
help="Color for onboard not paid reservations in the planning.",
|
|
default="rgba(255,64,64)",
|
|
)
|
|
|
|
paid_checkin_reservation_color = fields.Char(
|
|
string="Paid Checkin",
|
|
help="Color for onboard paid reservations in the planning.",
|
|
default="rgba(130,191,7)",
|
|
)
|
|
|
|
out_reservation_color = fields.Char(
|
|
string="Checkout",
|
|
help="Color for done not paid reservations in the planning.",
|
|
default="rgba(88,77,118)",
|
|
)
|
|
|
|
staff_reservation_color = fields.Char(
|
|
string="Staff",
|
|
help="Color for staff reservations in the planning.",
|
|
default="rgba(192,134,134)",
|
|
)
|
|
|
|
to_assign_reservation_color = fields.Char(
|
|
string="OTA Reservation To Assign",
|
|
help="Color for to_assign reservations in the planning.",
|
|
default="rgba(237,114,46,)",
|
|
)
|
|
|
|
pending_payment_reservation_color = fields.Char(
|
|
string="Payment Pending",
|
|
help="Color for pending payment reservations in the planning.",
|
|
default="rgba(162,70,137)",
|
|
)
|
|
|
|
@api.depends_context(
|
|
"checkin",
|
|
"checkout",
|
|
"real_avail",
|
|
"room_type_id",
|
|
"ubication_id",
|
|
"capacity",
|
|
"amenity_ids",
|
|
"pricelist_id",
|
|
"class_id",
|
|
"overnight_rooms",
|
|
"current_lines",
|
|
)
|
|
def _compute_free_room_ids(self):
|
|
checkin = self._context["checkin"]
|
|
checkout = self._context["checkout"]
|
|
|
|
if isinstance(checkin, str):
|
|
checkin = datetime.datetime.strptime(
|
|
checkin, DEFAULT_SERVER_DATE_FORMAT
|
|
).date()
|
|
if isinstance(checkout, str):
|
|
checkout = datetime.datetime.strptime(
|
|
checkout, DEFAULT_SERVER_DATE_FORMAT
|
|
).date()
|
|
current_lines = self.env.context.get("current_lines", False)
|
|
if current_lines and not isinstance(current_lines, list):
|
|
current_lines = [current_lines]
|
|
|
|
pricelist_id = self.env.context.get("pricelist_id", False)
|
|
room_type_id = self.env.context.get("room_type_id", False)
|
|
class_id = self._context.get("class_id", False)
|
|
real_avail = self._context.get("real_avail", False)
|
|
overnight_rooms = self._context.get("overnight_rooms", False)
|
|
capacity = self._context.get("capacity", False)
|
|
for pms_property in self:
|
|
free_rooms = pms_property.get_real_free_rooms(
|
|
checkin, checkout, current_lines
|
|
)
|
|
if pricelist_id and not real_avail:
|
|
# TODO: only closed_departure take account checkout date!
|
|
domain_rules = [
|
|
("date", ">=", checkin),
|
|
("date", "<=", checkout),
|
|
("pms_property_id", "=", pms_property.id),
|
|
]
|
|
if room_type_id:
|
|
domain_rules.append(("room_type_id", "=", room_type_id))
|
|
|
|
pricelist = self.env["product.pricelist"].browse(pricelist_id)
|
|
if pricelist.availability_plan_id:
|
|
domain_rules.append(
|
|
("availability_plan_id", "=", pricelist.availability_plan_id.id)
|
|
)
|
|
rule_items = self.env["pms.availability.plan.rule"].search(
|
|
domain_rules
|
|
)
|
|
|
|
if len(rule_items) > 0:
|
|
room_types_to_remove = []
|
|
for item in rule_items:
|
|
if pricelist.availability_plan_id.any_rule_applies(
|
|
checkin, checkout, item
|
|
):
|
|
room_types_to_remove.append(item.room_type_id.id)
|
|
free_rooms = free_rooms.filtered(
|
|
lambda x: x.room_type_id.id not in room_types_to_remove
|
|
)
|
|
if class_id:
|
|
free_rooms = free_rooms.filtered(
|
|
lambda x: x.room_type_id.class_id.id == class_id
|
|
)
|
|
if overnight_rooms:
|
|
free_rooms = free_rooms.filtered(
|
|
lambda x: x.room_type_id.overnight_room
|
|
)
|
|
if capacity:
|
|
free_rooms = free_rooms.filtered(lambda x: x.capacity >= capacity)
|
|
if len(free_rooms) > 0:
|
|
pms_property.free_room_ids = free_rooms.ids
|
|
else:
|
|
pms_property.free_room_ids = False
|
|
|
|
def get_real_free_rooms(self, checkin, checkout, current_lines=False):
|
|
self.ensure_one()
|
|
Avail = self.env["pms.availability"]
|
|
target_rooms = (
|
|
self.env["pms.room"]
|
|
.with_context(active_test=True)
|
|
.search([("pms_property_id", "=", self.id)])
|
|
)
|
|
|
|
room_type_id = self.env.context.get("room_type_id", False)
|
|
if room_type_id:
|
|
target_rooms = target_rooms.filtered(
|
|
lambda r: r.room_type_id.id == room_type_id
|
|
)
|
|
capacity = self.env.context.get("capacity", False)
|
|
if capacity:
|
|
target_rooms = target_rooms.filtered(lambda r: r.capacity >= capacity)
|
|
|
|
ubication_id = self.env.context.get("ubication_id", False)
|
|
if ubication_id:
|
|
target_rooms = target_rooms.filtered(
|
|
lambda r: r.ubication_id.id == ubication_id
|
|
)
|
|
|
|
amenity_ids = self.env.context.get("amenity_ids", False)
|
|
if amenity_ids:
|
|
if amenity_ids and not isinstance(amenity_ids, list):
|
|
amenity_ids = [amenity_ids]
|
|
target_rooms = target_rooms.filtered(
|
|
lambda r: len(set(amenity_ids) - set(r.room_amenity_ids.ids)) == 0
|
|
)
|
|
|
|
if not current_lines:
|
|
current_lines = []
|
|
|
|
rooms_not_avail_ids = Avail.get_rooms_not_avail(
|
|
checkin=checkin,
|
|
checkout=checkout,
|
|
room_ids=target_rooms.ids,
|
|
pms_property_id=self.id,
|
|
current_lines=current_lines,
|
|
)
|
|
domain_rooms = [("id", "in", target_rooms.ids)]
|
|
if rooms_not_avail_ids:
|
|
domain_rooms.append(
|
|
("id", "not in", rooms_not_avail_ids),
|
|
)
|
|
return self.env["pms.room"].with_context(active_test=True).search(domain_rooms)
|
|
|
|
@api.depends_context(
|
|
"checkin",
|
|
"checkout",
|
|
"real_avail",
|
|
"room_type_id",
|
|
"ubication_id",
|
|
"capacity",
|
|
"amenity_ids",
|
|
"pricelist_id",
|
|
"class_id",
|
|
"overnight_rooms",
|
|
"current_lines",
|
|
)
|
|
def _compute_availability(self):
|
|
for record in self:
|
|
checkin = self._context["checkin"]
|
|
checkout = self._context["checkout"]
|
|
if isinstance(checkin, str):
|
|
checkin = datetime.datetime.strptime(
|
|
checkin, DEFAULT_SERVER_DATE_FORMAT
|
|
).date()
|
|
if isinstance(checkout, str):
|
|
checkout = datetime.datetime.strptime(
|
|
checkout, DEFAULT_SERVER_DATE_FORMAT
|
|
).date()
|
|
room_type_id = self.env.context.get("room_type_id", False)
|
|
pricelist_id = self.env.context.get("pricelist_id", False)
|
|
current_lines = self.env.context.get("current_lines", [])
|
|
class_id = self._context.get("class_id", False)
|
|
real_avail = self._context.get("real_avail", False)
|
|
overnight_rooms = self._context.get("overnight_rooms", False)
|
|
capacity = self._context.get("capacity", False)
|
|
pms_property = record.with_context(
|
|
checkin=checkin,
|
|
checkout=checkout,
|
|
room_type_id=room_type_id,
|
|
current_lines=current_lines,
|
|
pricelist_id=pricelist_id,
|
|
class_id=class_id,
|
|
real_avail=real_avail,
|
|
overnight_rooms=overnight_rooms,
|
|
capacity=capacity,
|
|
)
|
|
count_free_rooms = len(pms_property.free_room_ids)
|
|
if current_lines and not isinstance(current_lines, list):
|
|
current_lines = [current_lines]
|
|
|
|
domain_rules = [
|
|
("date", ">=", checkin),
|
|
("date", "<=", checkout),
|
|
("pms_property_id", "=", pms_property.id),
|
|
]
|
|
if room_type_id:
|
|
domain_rules.append(("room_type_id", "=", room_type_id))
|
|
|
|
pricelist = False
|
|
if pricelist_id:
|
|
pricelist = self.env["product.pricelist"].browse(pricelist_id)
|
|
if pricelist and pricelist.availability_plan_id and not real_avail:
|
|
domain_rules.append(
|
|
("availability_plan_id", "=", pricelist.availability_plan_id.id)
|
|
)
|
|
rule_groups = self.env["pms.availability.plan.rule"].read_group(
|
|
domain_rules,
|
|
["plan_avail:sum"],
|
|
["date:day"],
|
|
lazy=False,
|
|
)
|
|
if len(rule_groups) > 0:
|
|
# If in the group per day, some room type has the sale blocked,
|
|
# we must subtract from that day the availability of that room type
|
|
for group in rule_groups:
|
|
items = self.env["pms.availability.plan.rule"].search(
|
|
group["__domain"]
|
|
)
|
|
for item in items:
|
|
if pricelist.availability_plan_id.any_rule_applies(
|
|
checkin, checkout, item
|
|
):
|
|
group["plan_avail"] -= item.plan_avail
|
|
count_free_rooms = min(i["plan_avail"] for i in rule_groups)
|
|
record.availability = count_free_rooms
|
|
|
|
@api.model
|
|
def splitted_availability(
|
|
self,
|
|
checkin,
|
|
checkout,
|
|
pms_property_id,
|
|
room_type_id=False,
|
|
current_lines=False,
|
|
pricelist=False,
|
|
real_avail=False,
|
|
):
|
|
if isinstance(checkin, str):
|
|
checkin = datetime.datetime.strptime(
|
|
checkin, DEFAULT_SERVER_DATE_FORMAT
|
|
).date()
|
|
if isinstance(checkout, str):
|
|
checkout = datetime.datetime.strptime(
|
|
checkout, DEFAULT_SERVER_DATE_FORMAT
|
|
).date()
|
|
for date_iterator in [
|
|
checkin + datetime.timedelta(days=x)
|
|
for x in range(0, (checkout - checkin).days)
|
|
]:
|
|
pms_property = self.env["pms.property"].browse(pms_property_id)
|
|
pms_property = pms_property.with_context(
|
|
checkin=date_iterator,
|
|
checkout=date_iterator + datetime.timedelta(1),
|
|
room_type_id=room_type_id,
|
|
current_lines=current_lines,
|
|
pricelist_id=pricelist.id,
|
|
real_avail=real_avail,
|
|
)
|
|
|
|
if len(pms_property.free_room_ids) < 1:
|
|
return False
|
|
return True
|
|
|
|
@api.constrains("ref")
|
|
def _check_unique_property_ref(self):
|
|
for record in self:
|
|
if record.ref:
|
|
duplicated = self.env["pms.property"].search(
|
|
[("ref", "=", record.ref), ("id", "!=", record.id)]
|
|
)
|
|
if duplicated:
|
|
raise ValidationError(
|
|
_(
|
|
"Alreay exist other property with this ref: %s (%s)",
|
|
duplicated.name,
|
|
duplicated.ref,
|
|
)
|
|
)
|
|
|
|
@api.constrains("pms_property_code")
|
|
def _check_unique_property_code(self):
|
|
for record in self:
|
|
if record.pms_property_code:
|
|
duplicated = self.env["pms.property"].search(
|
|
[
|
|
("pms_property_code", "=", record.pms_property_code),
|
|
("id", "!=", record.id),
|
|
]
|
|
)
|
|
if duplicated:
|
|
raise ValidationError(
|
|
_(
|
|
"Alreay exist other property with this code: %s (%s)",
|
|
duplicated.name,
|
|
duplicated.pms_property_code,
|
|
)
|
|
)
|
|
|
|
@api.constrains("default_arrival_hour")
|
|
def _check_arrival_hour(self):
|
|
for record in self:
|
|
try:
|
|
time.strptime(record.default_arrival_hour, "%H:%M")
|
|
return True
|
|
except ValueError:
|
|
raise ValidationError(
|
|
_(
|
|
"Format Arrival Hour (HH:MM) Error: %s",
|
|
record.default_arrival_hour,
|
|
)
|
|
)
|
|
|
|
@api.constrains("default_departure_hour")
|
|
def _check_departure_hour(self):
|
|
for record in self:
|
|
try:
|
|
time.strptime(record.default_departure_hour, "%H:%M")
|
|
return True
|
|
except ValueError:
|
|
raise ValidationError(
|
|
_(
|
|
"Format Departure Hour (HH:MM) Error: %s",
|
|
record.default_departure_hour,
|
|
)
|
|
)
|
|
|
|
def date_property_timezone(self, dt):
|
|
self.ensure_one()
|
|
if self.env.user:
|
|
tz_property = self.tz
|
|
dt = pytz.timezone(tz_property).localize(dt)
|
|
dt = dt.replace(tzinfo=None)
|
|
dt = pytz.timezone(self.env.user.tz or "UTC").localize(dt)
|
|
dt = dt.astimezone(pytz.utc)
|
|
dt = dt.replace(tzinfo=None)
|
|
return dt
|
|
|
|
def _get_payment_methods(self, automatic_included=False):
|
|
# We use automatic_included to True to see absolutely
|
|
# all the journals with associated payments, if it is
|
|
# false, we will only see those journals that can be used
|
|
# to pay manually
|
|
self.ensure_one()
|
|
payment_methods = self.env["account.journal"].search(
|
|
[
|
|
("type", "in", ["cash", "bank"]),
|
|
"|",
|
|
("pms_property_ids", "in", self.id),
|
|
"|",
|
|
"&",
|
|
("pms_property_ids", "=", False),
|
|
("company_id", "=", self.company_id.id),
|
|
"&",
|
|
("pms_property_ids", "=", False),
|
|
("company_id", "=", False),
|
|
]
|
|
)
|
|
if not automatic_included:
|
|
payment_methods = payment_methods.filtered(lambda p: p.allowed_pms_payments)
|
|
return payment_methods
|
|
|
|
@api.model
|
|
def create(self, vals):
|
|
name = vals.get("name")
|
|
if "folio_sequence_id" not in vals or not vals.get("folio_sequence_id"):
|
|
folio_sequence = self.env["ir.sequence"].create(
|
|
{
|
|
"name": "PMS Folio " + name,
|
|
"code": "pms.folio",
|
|
"prefix": "F/%(y)s",
|
|
"suffix": "%(sec)s",
|
|
"padding": 4,
|
|
"company_id": vals.get("company_id"),
|
|
}
|
|
)
|
|
vals.update({"folio_sequence_id": folio_sequence.id})
|
|
if "checkin_sequence_id" not in vals or not vals.get("checkin_sequence_id"):
|
|
checkin_sequence = self.env["ir.sequence"].create(
|
|
{
|
|
"name": "PMS Checkin " + name,
|
|
"code": "pms.checkin.partner",
|
|
"prefix": "C/%(y)s",
|
|
"suffix": "%(sec)s",
|
|
"padding": 4,
|
|
"company_id": vals.get("company_id"),
|
|
}
|
|
)
|
|
vals.update({"checkin_sequence_id": checkin_sequence.id})
|
|
record = super(PmsProperty, self).create(vals)
|
|
return record
|
|
|
|
@api.model
|
|
def daily_closing(
|
|
self, pms_property_ids, room_type_ids=False, availability_plan_ids=False
|
|
):
|
|
"""
|
|
This method is used to close the daily availability of rooms
|
|
"""
|
|
pms_properties = self.browse(pms_property_ids)
|
|
for pms_property in pms_properties:
|
|
if not room_type_ids:
|
|
room_type_ids = (
|
|
self.env["pms.room.type"]
|
|
.search(
|
|
[
|
|
"|",
|
|
("pms_property_ids", "in", pms_property.id),
|
|
("pms_property_ids", "=", False),
|
|
]
|
|
)
|
|
.ids
|
|
)
|
|
if not availability_plan_ids:
|
|
availability_plan_ids = (
|
|
self.env["pms.availability.plan"]
|
|
.search(
|
|
[
|
|
"|",
|
|
("pms_property_ids", "in", pms_property.id),
|
|
("pms_property_ids", "=", False),
|
|
]
|
|
)
|
|
.ids
|
|
)
|
|
for room_type in self.env["pms.room.type"].browse(room_type_ids):
|
|
for availability_plan in self.env["pms.availability.plan"].browse(
|
|
availability_plan_ids
|
|
):
|
|
rule = self.env["pms.availability.plan.rule"].search(
|
|
[
|
|
("pms_property_id", "=", pms_property.id),
|
|
("room_type_id", "=", room_type.id),
|
|
("availability_plan_id", "=", availability_plan.id),
|
|
("date", "=", fields.date.today()),
|
|
]
|
|
)
|
|
if not rule:
|
|
rule = self.env["pms.availability.plan.rule"].create(
|
|
{
|
|
"pms_property_id": pms_property.id,
|
|
"room_type_id": room_type.id,
|
|
"availability_plan_id": availability_plan.id,
|
|
"date": fields.date.today(),
|
|
"closed": True,
|
|
}
|
|
)
|
|
elif not rule.closed:
|
|
rule.write(
|
|
{
|
|
"closed": True,
|
|
}
|
|
)
|
|
return True
|
|
|
|
@api.model
|
|
def autoinvoicing(self, offset=0, with_delay=False, autocommit=False):
|
|
"""
|
|
This method is used to invoicing automatically the folios
|
|
and validate the draft invoices created by the folios
|
|
"""
|
|
date_reference = fields.Date.today() - relativedelta(days=offset)
|
|
# REVIEW: We clean the autoinvoice_date of the past draft invoices
|
|
# to avoid blocking the autoinvoicing
|
|
self.clean_date_on_past_draft_invoices(date_reference)
|
|
# 1- Invoicing the folios
|
|
folios = self.env["pms.folio"].search(
|
|
[
|
|
("sale_line_ids.autoinvoice_date", "=", date_reference),
|
|
("invoice_status", "=", "to_invoice"),
|
|
("amount_total", ">", 0),
|
|
]
|
|
)
|
|
paid_folios = folios.filtered(lambda f: f.pending_amount <= 0)
|
|
unpaid_folios = folios.filtered(lambda f: f.pending_amount > 0)
|
|
folios_to_invoice = paid_folios
|
|
# If the folio is unpaid we will auto invoice only the
|
|
# not cancelled lines
|
|
for folio in unpaid_folios:
|
|
if any([res.state != "cancel" for res in folio.reservation_ids]):
|
|
folios_to_invoice += folio
|
|
else:
|
|
folio.sudo().message_post(
|
|
body=_(
|
|
"Not invoiced due to pending amounts and cancelled reservations"
|
|
)
|
|
)
|
|
for folio in folios_to_invoice:
|
|
if with_delay:
|
|
self.with_delay().autoinvoice_folio(folio)
|
|
else:
|
|
self.autoinvoice_folio(folio)
|
|
# 2- Validate the draft invoices created by the folios
|
|
draft_invoices_to_post = self.env["account.move"].search(
|
|
[
|
|
("state", "=", "draft"),
|
|
("invoice_date_due", "=", date_reference),
|
|
("folio_ids", "!=", False),
|
|
]
|
|
)
|
|
for invoice in draft_invoices_to_post:
|
|
if with_delay:
|
|
self.with_delay().autovalidate_folio_invoice(invoice)
|
|
else:
|
|
self.autovalidate_folio_invoice(invoice)
|
|
|
|
# 3- Reverse the downpayment invoices that not was included in final invoice
|
|
downpayments_invoices_to_reverse = self.env["account.move.line"].search(
|
|
[
|
|
("move_id.state", "=", "posted"),
|
|
("folio_line_ids.is_downpayment", "=", True),
|
|
("folio_line_ids.qty_invoiced", ">", 0),
|
|
("folio_ids", "in", folios.ids),
|
|
]
|
|
)
|
|
downpayment_invoices = downpayments_invoices_to_reverse.mapped("move_id")
|
|
if downpayment_invoices:
|
|
for downpayment_invoice in downpayment_invoices:
|
|
default_values_list = [
|
|
{
|
|
"ref": _(f'Reversal of: {move.name + " - " + move.ref}'),
|
|
}
|
|
for move in downpayment_invoice
|
|
]
|
|
downpayment_invoice.with_context(
|
|
{"sii_refund_type": "I"}
|
|
)._reverse_moves(default_values_list, cancel=True)
|
|
downpayment_invoice.message_post(
|
|
body=_(
|
|
"""
|
|
The downpayment invoice has been reversed
|
|
because it was not included in the final invoice
|
|
"""
|
|
)
|
|
)
|
|
|
|
return True
|
|
|
|
@api.model
|
|
def clean_date_on_past_draft_invoices(self, date_reference):
|
|
"""
|
|
This method is used to clean the date on past draft invoices
|
|
"""
|
|
journal_ids = (
|
|
self.env["account.journal"]
|
|
.search(
|
|
[
|
|
("type", "=", "sale"),
|
|
("pms_property_ids", "!=", False),
|
|
]
|
|
)
|
|
.ids
|
|
)
|
|
draft_invoices = self.env["account.move"].search(
|
|
[
|
|
("state", "=", "draft"),
|
|
("invoice_date", "<", date_reference),
|
|
("journal_id", "in", journal_ids),
|
|
]
|
|
)
|
|
if draft_invoices:
|
|
draft_invoices.write({"invoice_date": date_reference})
|
|
return True
|
|
|
|
def autovalidate_folio_invoice(self, invoice):
|
|
try:
|
|
with self.env.cr.savepoint():
|
|
invoice.action_post()
|
|
except Exception as e:
|
|
invoice.message_post(body=_("Error in autovalidate invoice: " + str(e)))
|
|
|
|
def autoinvoice_folio(self, folio):
|
|
try:
|
|
with self.env.cr.savepoint():
|
|
# REVIEW: folio sale line "_compute_auotinvoice_date" sometimes
|
|
# dont work in services (probably cache issue¿?), we ensure that the date is
|
|
# set or recompute this
|
|
for line in folio.sale_line_ids.filtered(
|
|
lambda l: not l.autoinvoice_date
|
|
):
|
|
line._compute_autoinvoice_date()
|
|
invoices = folio.with_context(autoinvoice=True)._create_invoices(
|
|
grouped=True,
|
|
final=False,
|
|
)
|
|
downpayments = folio.sale_line_ids.filtered(
|
|
lambda l: l.is_downpayment and l.qty_invoiced > 0
|
|
)
|
|
for invoice in invoices:
|
|
if (
|
|
invoice.amount_total
|
|
> invoice.pms_property_id.max_amount_simplified_invoice
|
|
and invoice.journal_id.is_simplified_invoice
|
|
):
|
|
hosts_to_invoice = (
|
|
invoice.folio_ids.partner_invoice_ids.filtered(
|
|
lambda p: p._check_enought_invoice_data()
|
|
).mapped("id")
|
|
)
|
|
if hosts_to_invoice:
|
|
invoice.partner_id = hosts_to_invoice[0]
|
|
invoice.journal_id = (
|
|
invoice.pms_property_id.journal_normal_invoice_id
|
|
)
|
|
else:
|
|
mens = _(
|
|
"The total amount of the simplified invoice is higher than the "
|
|
"maximum amount allowed for simplified invoices, and dont have "
|
|
"enought data in hosts to create a normal invoice."
|
|
)
|
|
folio.sudo().message_post(body=mens)
|
|
raise ValidationError(mens)
|
|
for downpayment in downpayments.filtered(
|
|
lambda d: d.default_invoice_to == invoice.partner_id
|
|
):
|
|
# If the downpayment invoice partner is the same that the
|
|
# folio partner, we include the downpayment in the normal invoice
|
|
invoice_down_payment_vals = downpayment._prepare_invoice_line(
|
|
sequence=max(invoice.invoice_line_ids.mapped("sequence"))
|
|
+ 1,
|
|
)
|
|
invoice.write(
|
|
{"invoice_line_ids": [(0, 0, invoice_down_payment_vals)]}
|
|
)
|
|
invoice.action_post()
|
|
# The downpayment invoices that not was included in final invoice, are reversed
|
|
downpayment_invoices = (
|
|
downpayments.filtered(
|
|
lambda d: d.qty_invoiced > 0
|
|
).invoice_lines.mapped("move_id")
|
|
).filtered(lambda i: i.is_simplified_invoice)
|
|
if downpayment_invoices:
|
|
default_values_list = [
|
|
{
|
|
"ref": _(f'Reversal of: {move.name + " - " + move.ref}'),
|
|
}
|
|
for move in downpayment_invoices
|
|
]
|
|
downpayment_invoices.with_context(
|
|
{"sii_refund_type": "I"}
|
|
)._reverse_moves(default_values_list, cancel=True)
|
|
except Exception as e:
|
|
folio.sudo().message_post(body=_("Error in autoinvoicing folio: " + str(e)))
|
|
|
|
@api.constrains("journal_normal_invoice_id")
|
|
def _check_journal_normal_invoice(self):
|
|
for pms_property in self.filtered("journal_normal_invoice_id"):
|
|
if pms_property.journal_normal_invoice_id.is_simplified_invoice:
|
|
raise ValidationError(
|
|
_("Journal %s is not allowed to be used for normal invoices")
|
|
% pms_property.journal_normal_invoice_id.name
|
|
)
|
|
|
|
@api.constrains("journal_simplified_invoice_id")
|
|
def _check_journal_simplified_invoice(self):
|
|
for pms_property in self.filtered("journal_simplified_invoice_id"):
|
|
if not pms_property.journal_simplified_invoice_id.is_simplified_invoice:
|
|
pms_property.journal_simplified_invoice_id.is_simplified_invoice = True
|
|
|
|
@api.model
|
|
def _get_folio_default_journal(self, partner_invoice_id):
|
|
self.ensure_one()
|
|
partner = self.env["res.partner"].browse(partner_invoice_id)
|
|
if (
|
|
not partner
|
|
or partner.id == self.env.ref("pms.various_pms_partner").id
|
|
or (
|
|
not partner._check_enought_invoice_data()
|
|
and self._context.get("autoinvoice")
|
|
)
|
|
):
|
|
return self.journal_simplified_invoice_id
|
|
return self.journal_normal_invoice_id
|
|
|
|
def _get_adr(self, start_date, end_date, domain=False):
|
|
"""
|
|
Calculate monthly ADR for a property
|
|
:param start_date: start date
|
|
:param pms_property_id: pms property id
|
|
:param domain: domain to filter reservations (channel, agencies, etc...)
|
|
"""
|
|
self.ensure_one()
|
|
domain = [] if not domain else domain
|
|
domain.extend(
|
|
[
|
|
("pms_property_id", "=", self.id),
|
|
("occupies_availability", "=", True),
|
|
("reservation_id.reservation_type", "=", "normal"),
|
|
("date", ">=", start_date),
|
|
("date", "<=", end_date),
|
|
]
|
|
)
|
|
group_adr = self.env["pms.reservation.line"].read_group(
|
|
domain,
|
|
["price:avg"],
|
|
["date:day"],
|
|
)
|
|
if not len(group_adr):
|
|
return 0
|
|
adr = 0
|
|
for day_adr in group_adr:
|
|
adr += day_adr["price"]
|
|
|
|
return round(adr / len(group_adr), 2)
|
|
|
|
def _get_revpar(self, start_date, end_date, domain=False):
|
|
"""
|
|
Calculate monthly revpar for a property only in INE rooms
|
|
:param start_date: start date
|
|
:param pms_property_id: pms property id
|
|
:param domain: domain to filter reservations (channel, agencies, etc...)
|
|
"""
|
|
self.ensure_one()
|
|
domain = [] if not domain else domain
|
|
domain.extend(
|
|
[
|
|
("pms_property_id", "=", self.id),
|
|
("occupies_availability", "=", True),
|
|
("room_id.in_ine", "=", True),
|
|
("date", ">=", start_date),
|
|
("date", "<=", end_date),
|
|
]
|
|
)
|
|
price_domain = expression.AND(
|
|
[domain, [("reservation_id.reservation_type", "=", "normal")]]
|
|
)
|
|
sum_group_price = self.env["pms.reservation.line"].read_group(
|
|
price_domain,
|
|
["price"],
|
|
[],
|
|
)
|
|
not_allowed_rooms_domain = expression.AND(
|
|
[
|
|
domain,
|
|
[("reservation_id.reservation_type", "!=", "normal")],
|
|
]
|
|
)
|
|
count_room_days_not_allowed = len(
|
|
self.env["pms.reservation.line"].search(not_allowed_rooms_domain)
|
|
)
|
|
date_range_days = (end_date - start_date).days + 1
|
|
count_total_room_days = len(self.room_ids) * date_range_days
|
|
count_available_room_days = count_total_room_days - count_room_days_not_allowed
|
|
if not sum_group_price[0]["price"]:
|
|
return 0
|
|
revpar = round(sum_group_price[0]["price"] / count_available_room_days, 2)
|
|
return revpar
|
|
|
|
@api.model
|
|
def _name_search(
|
|
self, name, args=None, operator="ilike", limit=100, name_get_uid=None
|
|
):
|
|
args = args or []
|
|
domain = []
|
|
if name:
|
|
domain = [
|
|
"|",
|
|
"|",
|
|
("ref", "=ilike", name.split(" ")[0] + "%"),
|
|
("pms_property_code", "=ilike", name.split(" ")[0] + "%"),
|
|
("name", operator, name),
|
|
]
|
|
if operator in expression.NEGATIVE_TERM_OPERATORS:
|
|
domain = ["&", "!"] + domain[1:]
|
|
return self._search(
|
|
expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid
|
|
)
|
|
|
|
def name_get(self):
|
|
result = []
|
|
for record in self:
|
|
if self.env.context.get("only_code", False) and record.pms_property_code:
|
|
result.append((record.id, record.pms_property_code))
|
|
elif (
|
|
self.env.context.get("only_name", False) or not record.pms_property_code
|
|
):
|
|
result.append((record.id, record.name))
|
|
else:
|
|
result.append(
|
|
(record.id, "[" + record.pms_property_code + "] " + record.name)
|
|
)
|
|
return result
|