mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
Merge pull request #19 from commitsun/14.0-availability-plans
14.0 availability plans
This commit is contained in:
@@ -29,7 +29,7 @@ This module is an all-in-one property management system (PMS) focused on medium-
|
||||
for managing every aspect of your property's daily operations.
|
||||
|
||||
You can manage hotel properties with multi-hotel and multi-company support, including your rooms inventory,
|
||||
reservations, check-in, daily reports, board services, rate and restriction plans among other hotel functionalities.
|
||||
reservations, check-in, daily reports, board services, rate and availability plans among other hotel functionalities.
|
||||
|
||||
.. IMPORTANT::
|
||||
This is an alpha version, the data model and design can change at any time without warning.
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
"views/account_move_views.xml",
|
||||
"views/res_users_views.xml",
|
||||
"views/pms_room_type_class_views.xml",
|
||||
"views/pms_room_type_restriction_views.xml",
|
||||
"views/pms_room_type_restriction_item_views.xml",
|
||||
"views/pms_room_type_availability_views.xml",
|
||||
"views/pms_room_type_availability_rule_views.xml",
|
||||
"views/pms_service_views.xml",
|
||||
"views/pms_service_line_views.xml",
|
||||
"views/pms_shared_room_views.xml",
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Basic pms -->
|
||||
<record id="main_pms_room_type_restriction" model="pms.room.type.restriction">
|
||||
<field name="name">Restriction Plan</field>
|
||||
<record id="main_pms_room_type_availability" model="pms.room.type.availability">
|
||||
<field name="name">Availability Plan</field>
|
||||
</record>
|
||||
<record id="main_pms_property" model="pms.property">
|
||||
<field name="name">My Property</field>
|
||||
<field name="company_id" ref="base.main_company" />
|
||||
<field name="default_pricelist_id" ref="product.list0" />
|
||||
<field name="default_restriction_id" ref="main_pms_room_type_restriction" />
|
||||
<field
|
||||
name="default_availability_id"
|
||||
ref="main_pms_room_type_availability"
|
||||
/>
|
||||
<field name="street">Rua Street Demo, s/n</field>
|
||||
<field name="city">Commitsun city</field>
|
||||
<field name="country_id" ref="base.es" />
|
||||
|
||||
@@ -298,14 +298,17 @@
|
||||
</field>
|
||||
</record>
|
||||
<!-- Multi pms Demo -->
|
||||
<record id="demo_pms_room_type_restriction" model="pms.room.type.restriction">
|
||||
<field name="name">Restriction Plan Demo</field>
|
||||
<record id="demo_pms_room_type_availability" model="pms.room.type.availability">
|
||||
<field name="name">Availability Plan Demo</field>
|
||||
</record>
|
||||
<record id="demo_pms_property" model="pms.property">
|
||||
<field name="name">My pms Demo</field>
|
||||
<field name="company_id" ref="base.main_company" />
|
||||
<field name="default_pricelist_id" ref="product.list0" />
|
||||
<field name="default_restriction_id" ref="demo_pms_room_type_restriction" />
|
||||
<field
|
||||
name="default_availability_id"
|
||||
ref="demo_pms_room_type_availability"
|
||||
/>
|
||||
</record>
|
||||
<!-- pms.room.type -->
|
||||
<record id="demo_pms_room_type_0" model="pms.room.type">
|
||||
|
||||
@@ -24,8 +24,7 @@ from . import product_template
|
||||
from . import res_company
|
||||
from . import account_payment
|
||||
from . import pms_room_type_availability
|
||||
from . import pms_room_type_restriction
|
||||
from . import pms_room_type_restriction_item
|
||||
from . import pms_room_type_availability_rule
|
||||
from . import pms_reservation_line
|
||||
from . import pms_checkin_partner
|
||||
from . import product_pricelist
|
||||
|
||||
@@ -41,11 +41,11 @@ class PmsProperty(models.Model):
|
||||
required=True,
|
||||
help="The default pricelist used in this property.",
|
||||
)
|
||||
default_restriction_id = fields.Many2one(
|
||||
"pms.room.type.restriction",
|
||||
"Restriction Plan",
|
||||
default_availability_id = fields.Many2one(
|
||||
"pms.room.type.availability",
|
||||
"Availability Plan",
|
||||
required=True,
|
||||
help="The default restriction plan used in this property.",
|
||||
help="The default availability plan used in this property.",
|
||||
)
|
||||
default_arrival_hour = fields.Char(
|
||||
"Arrival Hour (GMT)", help="HH:mm Format", default="14:00"
|
||||
|
||||
@@ -507,7 +507,11 @@ class PmsReservation(models.Model):
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"reservation_line_ids.date", "overbooking", "state", "preferred_room_id"
|
||||
"reservation_line_ids.date",
|
||||
"overbooking",
|
||||
"state",
|
||||
"preferred_room_id",
|
||||
"pricelist_id",
|
||||
)
|
||||
def _compute_allowed_room_ids(self):
|
||||
for reservation in self:
|
||||
@@ -524,6 +528,7 @@ class PmsReservation(models.Model):
|
||||
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
|
||||
|
||||
@@ -1102,6 +1107,7 @@ class PmsReservation(models.Model):
|
||||
checkin=self.checkin,
|
||||
checkout=self.checkout,
|
||||
current_lines=self.reservation_line_ids.ids,
|
||||
pricelist=self.pricelist_id.id,
|
||||
)
|
||||
# REVIEW: check capacity room
|
||||
return {
|
||||
@@ -1184,6 +1190,7 @@ class PmsReservation(models.Model):
|
||||
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]
|
||||
|
||||
@@ -80,6 +80,12 @@ class PmsReservationLine(models.Model):
|
||||
store=True,
|
||||
help="This record is taken into account to calculate availability",
|
||||
)
|
||||
impacts_quota = fields.Integer(
|
||||
string="Impacts quota",
|
||||
compute="_compute_impact_quota",
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
@@ -105,14 +111,14 @@ class PmsReservationLine(models.Model):
|
||||
rooms_available = self.env[
|
||||
"pms.room.type.availability"
|
||||
].rooms_available(
|
||||
checkin=reservation.checkin,
|
||||
checkout=reservation.checkout,
|
||||
checkin=line.reservation_id.checkin,
|
||||
checkout=line.reservation_id.checkout,
|
||||
room_type_id=reservation.room_type_id.id
|
||||
if not free_room_select
|
||||
else False,
|
||||
current_lines=line._origin.reservation_id.reservation_line_ids.ids,
|
||||
pricelist=line.reservation_id.pricelist_id.id,
|
||||
)
|
||||
|
||||
# if there is availability for the entire stay
|
||||
if rooms_available:
|
||||
|
||||
@@ -134,9 +140,20 @@ class PmsReservationLine(models.Model):
|
||||
# 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.room.type.availability"].splitted_availability(
|
||||
checkin=line.reservation_id.checkin,
|
||||
checkout=line.reservation_id.checkout,
|
||||
room_type_id=line.reservation_id.room_type_id.id,
|
||||
current_lines=line._origin.reservation_id.reservation_line_ids.ids,
|
||||
pricelist=line.reservation_id.pricelist_id,
|
||||
):
|
||||
raise ValidationError(
|
||||
_("%s: No room type available")
|
||||
% (line.reservation_id.room_type_id.name)
|
||||
)
|
||||
|
||||
# if there is no availability for the entire stay without
|
||||
# changing rooms (we assume a split reservation)
|
||||
# the reservation can be allocated into several rooms
|
||||
else:
|
||||
rooms_ranking = dict()
|
||||
|
||||
@@ -173,12 +190,8 @@ class PmsReservationLine(models.Model):
|
||||
if room.id not in rooms_ranking
|
||||
else rooms_ranking[room.id] + 1
|
||||
)
|
||||
if len(rooms_ranking) == 0:
|
||||
raise ValidationError(
|
||||
_("%s: No room type available")
|
||||
% (reservation.room_type_id.name)
|
||||
)
|
||||
else:
|
||||
|
||||
if len(rooms_ranking) > 0:
|
||||
# we get the best score in the ranking
|
||||
best = max(rooms_ranking.values())
|
||||
|
||||
@@ -218,6 +231,17 @@ class PmsReservationLine(models.Model):
|
||||
# no matter what it is
|
||||
line.room_id = list(bests.keys())[0]
|
||||
|
||||
@api.depends("reservation_id.room_type_id", "reservation_id.pricelist_id")
|
||||
def _compute_impact_quota(self):
|
||||
for line in self:
|
||||
reservation = line.reservation_id
|
||||
line.impacts_quota = self.env["pms.room.type.availability"].update_quota(
|
||||
pricelist_id=reservation.pricelist_id,
|
||||
room_type_id=reservation.room_type_id,
|
||||
date=line.date,
|
||||
line=line,
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"reservation_id",
|
||||
"reservation_id.pricelist_id",
|
||||
|
||||
@@ -1,92 +1,193 @@
|
||||
# Copyright 2017 Alexandre Díaz
|
||||
# Copyright 2017 Dario Lodeiros
|
||||
# Copyright 2018 Pablo Quesada
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from datetime import timedelta
|
||||
import datetime
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class PmsRoomTypeAvailability(models.Model):
|
||||
"""The room type availability is used as a daily availability plan for room types
|
||||
and therefore is related only with one property."""
|
||||
|
||||
_name = "pms.room.type.availability"
|
||||
_description = "Availability"
|
||||
_inherit = "mail.thread"
|
||||
_description = "Reservation availability plan"
|
||||
|
||||
# Default methods
|
||||
@api.model
|
||||
def _default_max_avail(self):
|
||||
return self.room_type_id.default_max_avail
|
||||
|
||||
@api.model
|
||||
def _default_quota(self):
|
||||
return self.room_type_id.default_quota
|
||||
def _get_default_pms_property(self):
|
||||
return self.env.user.pms_property_id or None
|
||||
|
||||
# Fields declaration
|
||||
room_type_id = fields.Many2one(
|
||||
"pms.room.type", "Room Type", required=True, ondelete="cascade"
|
||||
)
|
||||
date = fields.Date(
|
||||
"Date",
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
quota = fields.Integer(
|
||||
"Quota",
|
||||
default=_default_quota,
|
||||
tracking=True,
|
||||
help="Generic Quota assigned.",
|
||||
)
|
||||
max_avail = fields.Integer(
|
||||
"Max. Availability",
|
||||
default=-1,
|
||||
readonly=True,
|
||||
tracking=True,
|
||||
help="Maximum simultaneous availability on own Booking Engine.",
|
||||
)
|
||||
no_web = fields.Boolean(
|
||||
"No Web",
|
||||
default=False,
|
||||
tracking=True,
|
||||
help="Set zero availability to the own Booking Engine "
|
||||
"even when the availability is positive,",
|
||||
name = fields.Char("Availability Plan Name", required=True)
|
||||
pms_property_id = fields.Many2one(
|
||||
comodel_name="pms.property",
|
||||
string="Property",
|
||||
ondelete="restrict",
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"unique_availability_room_type_rule_date",
|
||||
"unique(room_type_id, date)",
|
||||
"The availability rule for this date in this room type already exists, "
|
||||
"modify it instead of trying to create a new one",
|
||||
),
|
||||
]
|
||||
pms_pricelist_ids = fields.One2many(
|
||||
comodel_name="product.pricelist",
|
||||
inverse_name="availability_id",
|
||||
string="Pricelists",
|
||||
required=False,
|
||||
ondelete="restrict",
|
||||
)
|
||||
|
||||
item_ids = fields.One2many(
|
||||
comodel_name="pms.room.type.availability.rule",
|
||||
inverse_name="availability_id",
|
||||
string="Rule Items",
|
||||
copy=True,
|
||||
)
|
||||
|
||||
active = fields.Boolean(
|
||||
string="Active",
|
||||
default=True,
|
||||
help="If unchecked, it will allow you to hide the "
|
||||
"Availability plan without removing it.",
|
||||
)
|
||||
|
||||
# Business Methods
|
||||
@classmethod
|
||||
def any_rule_applies(cls, checkin, checkout, item):
|
||||
reservation_len = (checkout - checkin).days
|
||||
return any(
|
||||
[
|
||||
(0 < item.max_stay < reservation_len),
|
||||
(0 < item.min_stay > reservation_len),
|
||||
(0 < item.max_stay_arrival < reservation_len and checkin == item.date),
|
||||
(0 < item.min_stay_arrival > reservation_len and checkin == item.date),
|
||||
item.closed,
|
||||
(item.closed_arrival and checkin == item.date),
|
||||
(item.closed_departure and checkout == item.date),
|
||||
(item.quota == 0 or item.max_avail == 0),
|
||||
]
|
||||
)
|
||||
|
||||
@api.model
|
||||
def rooms_available(
|
||||
self, checkin, checkout, room_type_id=False, current_lines=False
|
||||
self,
|
||||
checkin,
|
||||
checkout,
|
||||
room_type_id=False,
|
||||
current_lines=False,
|
||||
pricelist=False,
|
||||
):
|
||||
domain = self._get_domain_reservations_occupation(
|
||||
dfrom=checkin,
|
||||
dto=checkout - timedelta(1),
|
||||
current_lines=current_lines,
|
||||
)
|
||||
reservation_lines = self.env["pms.reservation.line"].search(domain)
|
||||
reservations_rooms = reservation_lines.mapped("room_id.id")
|
||||
free_rooms = self.env["pms.room"].search([("id", "not in", reservations_rooms)])
|
||||
if room_type_id:
|
||||
rooms_linked = (
|
||||
self.env["pms.room.type"].search([("id", "=", room_type_id)]).room_ids
|
||||
if current_lines and not isinstance(current_lines, list):
|
||||
current_lines = [current_lines]
|
||||
|
||||
rooms_not_avail = (
|
||||
self.env["pms.reservation.line"]
|
||||
.search(
|
||||
[
|
||||
("date", ">=", checkin),
|
||||
("date", "<=", checkout - datetime.timedelta(1)),
|
||||
("occupies_availability", "=", True),
|
||||
("id", "not in", current_lines if current_lines else []),
|
||||
]
|
||||
)
|
||||
free_rooms = free_rooms & rooms_linked
|
||||
.mapped("room_id.id")
|
||||
)
|
||||
|
||||
domain_rooms = [
|
||||
("id", "not in", rooms_not_avail if len(rooms_not_avail) > 0 else [])
|
||||
]
|
||||
domain_rules = [
|
||||
("date", ">=", checkin),
|
||||
("date", "<=", checkout),
|
||||
]
|
||||
|
||||
if room_type_id:
|
||||
domain_rooms.append(("room_type_id", "=", room_type_id))
|
||||
domain_rules.append(("room_type_id", "=", room_type_id))
|
||||
|
||||
free_rooms = self.env["pms.room"].search(domain_rooms)
|
||||
|
||||
if pricelist:
|
||||
domain_rules.append(("availability_id.pms_pricelist_ids", "=", pricelist))
|
||||
rule_items = self.env["pms.room.type.availability.rule"].search(
|
||||
domain_rules
|
||||
)
|
||||
|
||||
if len(rule_items) > 0:
|
||||
room_types_to_remove = []
|
||||
for item in rule_items:
|
||||
if self.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
|
||||
)
|
||||
|
||||
return free_rooms.sorted(key=lambda r: r.sequence)
|
||||
|
||||
@api.model
|
||||
def _get_domain_reservations_occupation(self, dfrom, dto, current_lines=False):
|
||||
if current_lines and not isinstance(current_lines, list):
|
||||
current_lines = [current_lines]
|
||||
domain = [
|
||||
("date", ">=", dfrom),
|
||||
("date", "<=", dto),
|
||||
("occupies_availability", "=", True),
|
||||
("id", "not in", current_lines),
|
||||
]
|
||||
return domain
|
||||
def splitted_availability(
|
||||
self,
|
||||
checkin,
|
||||
checkout,
|
||||
room_type_id=False,
|
||||
current_lines=False,
|
||||
pricelist=False,
|
||||
):
|
||||
for date_iterator in [
|
||||
checkin + datetime.timedelta(days=x)
|
||||
for x in range(0, (checkout - checkin).days)
|
||||
]:
|
||||
rooms_avail = self.rooms_available(
|
||||
checkin=date_iterator,
|
||||
checkout=date_iterator + datetime.timedelta(1),
|
||||
room_type_id=room_type_id,
|
||||
current_lines=current_lines,
|
||||
pricelist=pricelist.id,
|
||||
)
|
||||
if len(rooms_avail) < 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def update_quota(self, pricelist_id, room_type_id, date, line):
|
||||
if pricelist_id and room_type_id and date:
|
||||
rule = self.env["pms.room.type.availability.rule"].search(
|
||||
[
|
||||
("availability_id.pms_pricelist_ids", "=", pricelist_id.id),
|
||||
("room_type_id", "=", room_type_id.id),
|
||||
("date", "=", date),
|
||||
]
|
||||
)
|
||||
# applies a rule
|
||||
if rule:
|
||||
rule.ensure_one()
|
||||
if rule and rule.quota != -1 and rule.quota > 0:
|
||||
|
||||
# the line has no rule item applied before
|
||||
if not line.impacts_quota:
|
||||
rule.quota -= 1
|
||||
return rule.id
|
||||
|
||||
# the line has a rule item applied before
|
||||
elif line.impacts_quota != rule.id:
|
||||
|
||||
# decrement quota on current rule item
|
||||
rule.quota -= 1
|
||||
|
||||
# check old rule item
|
||||
old_rule = self.env["pms.room.type.availability.rule"].search(
|
||||
[("id", "=", line.impacts_quota)]
|
||||
)
|
||||
|
||||
# restore quota in old rule item
|
||||
if old_rule:
|
||||
old_rule.quota += 1
|
||||
|
||||
return rule.id
|
||||
|
||||
# in any case, check old rule item
|
||||
if line.impacts_quota:
|
||||
old_rule = self.env["pms.room.type.availability.rule"].search(
|
||||
[("id", "=", line.impacts_quota)]
|
||||
)
|
||||
# and restore quota in old rule item
|
||||
if old_rule:
|
||||
old_rule.quota += 1
|
||||
|
||||
return False
|
||||
|
||||
116
pms/models/pms_room_type_availability_rule.py
Normal file
116
pms/models/pms_room_type_availability_rule.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# Copyright 2017 Alexandre Díaz
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class PmsRoomTypeAvailabilityRule(models.Model):
|
||||
_name = "pms.room.type.availability.rule"
|
||||
_description = "Reservation rule by day"
|
||||
|
||||
# Field Declarations
|
||||
|
||||
availability_id = fields.Many2one(
|
||||
comodel_name="pms.room.type.availability",
|
||||
string="Availability Plan",
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
)
|
||||
room_type_id = fields.Many2one(
|
||||
comodel_name="pms.room.type",
|
||||
string="Room Type",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
date = fields.Date(string="Date")
|
||||
|
||||
min_stay = fields.Integer(
|
||||
string="Min. Stay",
|
||||
default=0,
|
||||
)
|
||||
min_stay_arrival = fields.Integer(
|
||||
string="Min. Stay Arrival",
|
||||
default=0,
|
||||
)
|
||||
max_stay = fields.Integer(
|
||||
string="Max. Stay",
|
||||
default=0,
|
||||
)
|
||||
max_stay_arrival = fields.Integer(
|
||||
string="Max. Stay Arrival",
|
||||
default=0,
|
||||
)
|
||||
closed = fields.Boolean(
|
||||
string="Closed",
|
||||
default=False,
|
||||
)
|
||||
closed_departure = fields.Boolean(
|
||||
string="Closed Departure",
|
||||
default=False,
|
||||
)
|
||||
closed_arrival = fields.Boolean(
|
||||
string="Closed Arrival",
|
||||
default=False,
|
||||
)
|
||||
quota = fields.Integer(
|
||||
string="Quota",
|
||||
store=True,
|
||||
readonly=False,
|
||||
compute="_compute_quota",
|
||||
help="Generic Quota assigned.",
|
||||
)
|
||||
|
||||
max_avail = fields.Integer(
|
||||
string="Max. Availability",
|
||||
store=True,
|
||||
readonly=False,
|
||||
compute="_compute_max_avail",
|
||||
help="Maximum simultaneous availability on own Booking Engine.",
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"room_type_registry_unique",
|
||||
"unique(availability_id, room_type_id, date)",
|
||||
"Only can exists one availability rule in the same \
|
||||
day for the same room type!",
|
||||
)
|
||||
]
|
||||
|
||||
@api.depends("room_type_id")
|
||||
def _compute_quota(self):
|
||||
for record in self:
|
||||
if not record.quota:
|
||||
record.quota = record.room_type_id.default_quota
|
||||
|
||||
@api.depends("room_type_id")
|
||||
def _compute_max_avail(self):
|
||||
for record in self:
|
||||
if not record.max_avail:
|
||||
record.max_avail = record.room_type_id.default_max_avail
|
||||
|
||||
@api.constrains("min_stay", "min_stay_arrival", "max_stay", "max_stay_arrival")
|
||||
def _check_min_max_stay(self):
|
||||
for record in self:
|
||||
if record.min_stay < 0:
|
||||
raise ValidationError(_("Min. Stay can't be less than zero"))
|
||||
elif record.min_stay_arrival < 0:
|
||||
raise ValidationError(_("Min. Stay Arrival can't be less than zero"))
|
||||
elif record.max_stay < 0:
|
||||
raise ValidationError(_("Max. Stay can't be less than zero"))
|
||||
elif record.max_stay_arrival < 0:
|
||||
raise ValidationError(_("Max. Stay Arrival can't be less than zero"))
|
||||
elif (
|
||||
record.min_stay != 0
|
||||
and record.max_stay != 0
|
||||
and record.min_stay > record.max_stay
|
||||
):
|
||||
raise ValidationError(_("Max. Stay can't be less than Min. Stay"))
|
||||
elif (
|
||||
record.min_stay_arrival != 0
|
||||
and record.max_stay_arrival != 0
|
||||
and record.min_stay_arrival > record.max_stay_arrival
|
||||
):
|
||||
raise ValidationError(
|
||||
_("Max. Stay Arrival can't be less than Min. Stay Arrival")
|
||||
)
|
||||
@@ -1,37 +0,0 @@
|
||||
# Copyright 2017 Alexandre Díaz
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class PmsRoomTypeRestriction(models.Model):
|
||||
"""The room type restriction is used as a daily restriction plan for room types
|
||||
and therefore is related only with one property."""
|
||||
|
||||
_name = "pms.room.type.restriction"
|
||||
_description = "Reservation restriction plan"
|
||||
|
||||
# Default methods
|
||||
@api.model
|
||||
def _get_default_pms_property(self):
|
||||
return self.env.user.pms_property_id or None
|
||||
|
||||
# Fields declaration
|
||||
name = fields.Char("Restriction Plan Name", required=True)
|
||||
pms_property_id = fields.Many2one(
|
||||
"pms.property",
|
||||
"Property",
|
||||
ondelete="restrict",
|
||||
default=_get_default_pms_property,
|
||||
)
|
||||
item_ids = fields.One2many(
|
||||
"pms.room.type.restriction.item",
|
||||
"restriction_id",
|
||||
string="Restriction Items",
|
||||
copy=True,
|
||||
)
|
||||
active = fields.Boolean(
|
||||
"Active",
|
||||
default=True,
|
||||
help="If unchecked, it will allow you to hide the "
|
||||
"restriction plan without removing it.",
|
||||
)
|
||||
@@ -1,49 +0,0 @@
|
||||
# Copyright 2017 Alexandre Díaz
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class PmsRoomTypeRestrictionItem(models.Model):
|
||||
_name = "pms.room.type.restriction.item"
|
||||
_description = "Reservation restriction by day"
|
||||
|
||||
# Field Declarations
|
||||
restriction_id = fields.Many2one(
|
||||
"pms.room.type.restriction", "Restriction Plan", ondelete="cascade", index=True
|
||||
)
|
||||
room_type_id = fields.Many2one(
|
||||
"pms.room.type", "Room Type", required=True, ondelete="cascade"
|
||||
)
|
||||
date = fields.Date("Date")
|
||||
|
||||
min_stay = fields.Integer("Min. Stay")
|
||||
min_stay_arrival = fields.Integer("Min. Stay Arrival")
|
||||
max_stay = fields.Integer("Max. Stay")
|
||||
max_stay_arrival = fields.Integer("Max. Stay Arrival")
|
||||
closed = fields.Boolean("Closed")
|
||||
closed_departure = fields.Boolean("Closed Departure")
|
||||
closed_arrival = fields.Boolean("Closed Arrival")
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"room_type_registry_unique",
|
||||
"unique(restriction_id, room_type_id, date)",
|
||||
"Only can exists one restriction in the same \
|
||||
day for the same room type!",
|
||||
)
|
||||
]
|
||||
|
||||
# Constraints and onchanges
|
||||
|
||||
@api.constrains("min_stay", "min_stay_arrival", "max_stay", "max_stay_arrival")
|
||||
def _check_min_stay(self):
|
||||
for record in self:
|
||||
if record.min_stay < 0:
|
||||
raise ValidationError(_("Min. Stay can't be less than zero"))
|
||||
elif record.min_stay_arrival < 0:
|
||||
raise ValidationError(_("Min. Stay Arrival can't be less than zero"))
|
||||
elif record.max_stay < 0:
|
||||
raise ValidationError(_("Max. Stay can't be less than zero"))
|
||||
elif record.max_stay_arrival < 0:
|
||||
raise ValidationError(_("Max. Stay Arrival can't be less than zero"))
|
||||
@@ -22,6 +22,12 @@ class ProductPricelist(models.Model):
|
||||
[("daily", "Daily Plan")], string="Pricelist Type", default="daily"
|
||||
)
|
||||
|
||||
availability_id = fields.Many2one(
|
||||
comodel_name="pms.room.type.availability",
|
||||
string="Availability Plan",
|
||||
ondelete="restrict",
|
||||
)
|
||||
|
||||
# Constraints and onchanges
|
||||
# @api.constrains("pricelist_type", "pms_property_ids")
|
||||
# def _check_pricelist_type_property_ids(self):
|
||||
|
||||
@@ -2,4 +2,4 @@ This module is an all-in-one property management system (PMS) focused on medium-
|
||||
for managing every aspect of your property's daily operations.
|
||||
|
||||
You can manage hotel properties with multi-hotel and multi-company support, including your rooms inventory,
|
||||
reservations, check-in, daily reports, board services, rate and restriction plans among other hotel functionalities.
|
||||
reservations, check-in, daily reports, board services, rate and availability plans among other hotel functionalities.
|
||||
|
||||
@@ -3,7 +3,6 @@ user_access_pms_floor,user_access_pms_floor,model_pms_floor,pms.group_pms_user,1
|
||||
user_access_pms_amenity,user_access_pms_amenity,model_pms_amenity,pms.group_pms_user,1,0,0,0
|
||||
user_access_pms_amenity_type,user_access_pms_amenity_type,model_pms_amenity_type,pms.group_pms_user,1,0,0,0
|
||||
user_access_pms_service,user_access_pms_service,model_pms_service,pms.group_pms_user,1,1,1,1
|
||||
user_access_pms_room_type_restriction,user_access_pms_room_type_restriction,model_pms_room_type_restriction,pms.group_pms_user,1,0,0,0
|
||||
user_access_pms_reservation_line,user_access_pms_reservation_line,model_pms_reservation_line,pms.group_pms_user,1,1,1,1
|
||||
user_access_room_closure_reason,user_access_room_closure_reason,model_room_closure_reason,pms.group_pms_user,1,0,0,0
|
||||
user_access_pms_service_line,user_access_pms_service_line,model_pms_service_line,pms.group_pms_user,1,1,1,1
|
||||
@@ -12,7 +11,7 @@ user_access_pms_checkin_partner,user_access_pms_checkin_partner,model_pms_checki
|
||||
user_access_pms_room_type_class,user_access_pms_room_type_class,model_pms_room_type_class,pms.group_pms_user,1,0,0,0
|
||||
user_access_pms_room,user_access_pms_room,model_pms_room,pms.group_pms_user,1,0,0,0
|
||||
user_access_shared_pms_room,user_access_pms_shared_room,model_pms_shared_room,pms.group_pms_user,1,0,0,0
|
||||
user_access_pms_room_type_restriction_item,user_access_pms_room_type_restriction_item,model_pms_room_type_restriction_item,pms.group_pms_user,1,0,0,0
|
||||
user_access_pms_room_type_availability_rule,user_access_pms_room_type_availability_rule,model_pms_room_type_availability_rule,pms.group_pms_user,1,0,0,0
|
||||
user_access_pms_reservation,user_access_pms_reservation,model_pms_reservation,pms.group_pms_user,1,1,1,1
|
||||
user_access_pms_folio,user_access_pms_folio,model_pms_folio,pms.group_pms_user,1,1,1,1
|
||||
user_access_pms_room_type,user_access_pms_room_type,model_pms_room_type,pms.group_pms_user,1,0,0,0
|
||||
@@ -29,7 +28,6 @@ manager_access_pms_floor,manager_access_pms_floor,model_pms_floor,pms.group_pms_
|
||||
manager_access_pms_amenity,manager_access_pms_amenity,model_pms_amenity,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_amenity_type,manager_access_pms_amenity_type,model_pms_amenity_type,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_service,manager_access_pms_service,model_pms_service,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_room_type_restriction,manager_access_pms_room_type_restriction,model_pms_room_type_restriction,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_reservation_line,manager_access_pms_reservation_line,model_pms_reservation_line,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_room_closure_reason,manager_access_room_closure_reason,model_room_closure_reason,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_service_line,manager_access_pms_service_line,model_pms_service_line,pms.group_pms_manager,1,1,1,1
|
||||
@@ -38,7 +36,7 @@ manager_access_pms_checkin_partner,manager_access_pms_checkin_partner,model_pms_
|
||||
manager_access_pms_room_type_class,manager_access_pms_room_type_class,model_pms_room_type_class,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_room,manager_access_pms_room,model_pms_room,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_shared_room,manager_access_pms_shared_room,model_pms_shared_room,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_room_type_restriction_item,manager_access_pms_room_type_restriction_item,model_pms_room_type_restriction_item,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_room_type_availability_rule,manager_access_pms_room_type_availability_rule,model_pms_room_type_availability_rule,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_reservation,manager_access_pms_reservation,model_pms_reservation,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_folio,manager_access_pms_folio,model_pms_folio,pms.group_pms_manager,1,1,1,1
|
||||
manager_access_pms_room_type,manager_access_pms_room_type,model_pms_room_type,pms.group_pms_manager,1,1,1,1
|
||||
|
||||
|
@@ -371,7 +371,7 @@ ul.auto-toc {
|
||||
<p>This module is an all-in-one property management system (PMS) focused on medium-sized hotels
|
||||
for managing every aspect of your property’s daily operations.</p>
|
||||
<p>You can manage hotel properties with multi-hotel and multi-company support, including your rooms inventory,
|
||||
reservations, check-in, daily reports, board services, rate and restriction plans among other hotel functionalities.</p>
|
||||
reservations, check-in, daily reports, board services, rate and availability plans among other hotel functionalities.</p>
|
||||
<div class="admonition important">
|
||||
<p class="first admonition-title">Important</p>
|
||||
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
|
||||
|
||||
@@ -25,4 +25,5 @@ from . import test_pms_pricelist_priority
|
||||
from . import test_pms_checkin_partner
|
||||
from . import test_pms_sale_channel
|
||||
from . import test_pms_folio
|
||||
from . import test_pms_room_type
|
||||
from . import test_pms_room_type_availability_rules
|
||||
from . import test_pms_room_type
|
||||
@@ -15,19 +15,19 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
{"name": "Category1"}
|
||||
)
|
||||
|
||||
self.restriction = self.env["pms.room.type.restriction"].create(
|
||||
{"name": "Restriction1"}
|
||||
self.availability = self.env["pms.room.type.availability"].create(
|
||||
{"name": "Availability 1"}
|
||||
)
|
||||
|
||||
self.restriction2 = self.env["pms.room.type.restriction"].create(
|
||||
{"name": "Restriction2"}
|
||||
self.availability2 = self.env["pms.room.type.availability"].create(
|
||||
{"name": "Availability"}
|
||||
)
|
||||
self.property1 = self.env["pms.property"].create(
|
||||
{
|
||||
"name": "Property_1",
|
||||
"company_id": self.env.ref("base.main_company").id,
|
||||
"default_pricelist_id": self.env.ref("product.list0").id,
|
||||
"default_restriction_id": self.restriction.id,
|
||||
"default_availability_id": self.availability.id,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
"name": "Property_2",
|
||||
"company_id": self.env.ref("base.main_company").id,
|
||||
"default_pricelist_id": self.env.ref("product.list0").id,
|
||||
"default_restriction_id": self.restriction2.id,
|
||||
"default_availability_id": self.availability2.id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ from .common import TestHotel
|
||||
@freeze_time("2012-01-14")
|
||||
class TestPmsReservations(TestHotel):
|
||||
def create_common_scenario(self):
|
||||
# create a room type restriction
|
||||
self.room_type_restriction = self.env["pms.room.type.restriction"].create(
|
||||
{"name": "Restriction plan for TEST"}
|
||||
# create a room type availability
|
||||
self.room_type_availability = self.env["pms.room.type.availability"].create(
|
||||
{"name": "Availability plan for TEST"}
|
||||
)
|
||||
|
||||
# create a property
|
||||
@@ -22,7 +22,7 @@ class TestPmsReservations(TestHotel):
|
||||
"name": "MY PMS TEST",
|
||||
"company_id": self.env.ref("base.main_company").id,
|
||||
"default_pricelist_id": self.env.ref("product.list0").id,
|
||||
"default_restriction_id": self.room_type_restriction.id,
|
||||
"default_availability_id": self.room_type_availability.id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
568
pms/tests/test_pms_room_type_availability_rules.py
Normal file
568
pms/tests/test_pms_room_type_availability_rules.py
Normal file
@@ -0,0 +1,568 @@
|
||||
import datetime
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from .common import TestHotel
|
||||
|
||||
|
||||
@freeze_time("1980-01-01")
|
||||
class TestPmsRoomTypeAvailabilityRules(TestHotel):
|
||||
def create_common_scenario(self):
|
||||
# product.pricelist
|
||||
self.test_pricelist1 = self.env["product.pricelist"].create(
|
||||
{
|
||||
"name": "test pricelist 1",
|
||||
}
|
||||
)
|
||||
# pms.room.type.availability
|
||||
self.test_room_type_availability1 = self.env[
|
||||
"pms.room.type.availability"
|
||||
].create(
|
||||
{
|
||||
"name": "Availability plan for TEST",
|
||||
"pms_pricelist_ids": [(6, 0, [self.test_pricelist1.id])],
|
||||
}
|
||||
)
|
||||
# pms.property
|
||||
self.test_property = self.env["pms.property"].create(
|
||||
{
|
||||
"name": "MY PMS TEST",
|
||||
"company_id": self.env.ref("base.main_company").id,
|
||||
"default_pricelist_id": self.test_pricelist1.id,
|
||||
"default_availability_id": self.test_room_type_availability1.id,
|
||||
}
|
||||
)
|
||||
# pms.room.type.class
|
||||
self.test_room_type_class = self.env["pms.room.type.class"].create(
|
||||
{"name": "Room"}
|
||||
)
|
||||
|
||||
# pms.room.type
|
||||
self.test_room_type_single = self.env["pms.room.type"].create(
|
||||
{
|
||||
"pms_property_ids": [self.test_property.id],
|
||||
"name": "Single Test",
|
||||
"code_type": "SNG_Test",
|
||||
"class_id": self.test_room_type_class.id,
|
||||
}
|
||||
)
|
||||
# pms.room.type
|
||||
self.test_room_type_double = self.env["pms.room.type"].create(
|
||||
{
|
||||
"pms_property_ids": [self.test_property.id],
|
||||
"name": "Double Test",
|
||||
"code_type": "DBL_Test",
|
||||
"class_id": self.test_room_type_class.id,
|
||||
}
|
||||
)
|
||||
# pms.room
|
||||
self.test_room1_double = self.env["pms.room"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"name": "Double 201 test",
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"capacity": 2,
|
||||
}
|
||||
)
|
||||
# pms.room
|
||||
self.test_room2_double = self.env["pms.room"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"name": "Double 202 test",
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"capacity": 2,
|
||||
}
|
||||
)
|
||||
# pms.room
|
||||
# self.test_room3_double = self.env["pms.room"].create(
|
||||
# {
|
||||
# "pms_property_id": self.test_property.id,
|
||||
# "name": "Double 203 test",
|
||||
# "room_type_id": self.test_room_type_double.id,
|
||||
# "capacity": 2,
|
||||
# }
|
||||
# )
|
||||
# # pms.room
|
||||
# self.test_room4_double = self.env["pms.room"].create(
|
||||
# {
|
||||
# "pms_property_id": self.test_property.id,
|
||||
# "name": "Double 204 test",
|
||||
# "room_type_id": self.test_room_type_double.id,
|
||||
# "capacity": 2,
|
||||
# }
|
||||
# )
|
||||
# pms.room
|
||||
self.test_room1_single = self.env["pms.room"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"name": "Single 101 test",
|
||||
"room_type_id": self.test_room_type_single.id,
|
||||
"capacity": 1,
|
||||
}
|
||||
)
|
||||
# pms.room
|
||||
self.test_room2_single = self.env["pms.room"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"name": "Single 102 test",
|
||||
"room_type_id": self.test_room_type_single.id,
|
||||
"capacity": 1,
|
||||
}
|
||||
)
|
||||
|
||||
def test_availability_rooms_all(self):
|
||||
# TEST CASE
|
||||
# get availability withouth rules
|
||||
|
||||
# ARRANGE
|
||||
self.create_common_scenario()
|
||||
|
||||
checkin = fields.date.today()
|
||||
checkout = (fields.datetime.today() + datetime.timedelta(days=4)).date()
|
||||
test_rooms_double_rooms = self.env["pms.room"].search(
|
||||
[("pms_property_id", "=", self.test_property.id)]
|
||||
)
|
||||
|
||||
# ACT
|
||||
result = self.env["pms.room.type.availability"].rooms_available(
|
||||
checkin=checkin,
|
||||
checkout=checkout,
|
||||
)
|
||||
# ASSERT
|
||||
obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms)
|
||||
self.assertTrue(
|
||||
obtained,
|
||||
"Availability should contain the test rooms"
|
||||
"because there's no availability rules for them.",
|
||||
)
|
||||
|
||||
def test_availability_rooms_all_lines(self):
|
||||
# TEST CASE
|
||||
# get availability withouth rules
|
||||
# given reservation lines to not consider
|
||||
|
||||
# ARRANGE
|
||||
self.create_common_scenario()
|
||||
checkin = fields.date.today()
|
||||
checkout = (fields.datetime.today() + datetime.timedelta(days=4)).date()
|
||||
test_rooms_double_rooms = self.env["pms.room"].search(
|
||||
[("pms_property_id", "=", self.test_property.id)]
|
||||
)
|
||||
test_reservation = self.env["pms.reservation"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"checkin": checkin,
|
||||
"checkout": checkout,
|
||||
}
|
||||
)
|
||||
|
||||
# ACT
|
||||
result = self.env["pms.room.type.availability"].rooms_available(
|
||||
checkin=checkin,
|
||||
checkout=checkout,
|
||||
current_lines=test_reservation.reservation_line_ids.ids,
|
||||
)
|
||||
# ASSERT
|
||||
obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms)
|
||||
self.assertTrue(
|
||||
obtained,
|
||||
"Availability should contain the test rooms"
|
||||
"because there's no availability rules for them.",
|
||||
)
|
||||
|
||||
def test_availability_rooms_room_type(self):
|
||||
# TEST CASE
|
||||
# get availability withouth rules
|
||||
# given a room type
|
||||
|
||||
# ARRANGE
|
||||
self.create_common_scenario()
|
||||
test_rooms_double_rooms = self.env["pms.room"].search(
|
||||
[
|
||||
("pms_property_id", "=", self.test_property.id),
|
||||
("room_type_id", "=", self.test_room_type_double.id),
|
||||
]
|
||||
)
|
||||
|
||||
# ACT
|
||||
result = self.env["pms.room.type.availability"].rooms_available(
|
||||
checkin=fields.date.today(),
|
||||
checkout=(fields.datetime.today() + datetime.timedelta(days=4)).date(),
|
||||
room_type_id=self.test_room_type_double.id,
|
||||
)
|
||||
|
||||
# ASSERT
|
||||
obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms)
|
||||
self.assertTrue(
|
||||
obtained,
|
||||
"Availability should contain the test rooms"
|
||||
"because there's no availability rules for them.",
|
||||
)
|
||||
|
||||
def test_availability_closed_no_room_type(self):
|
||||
# TEST CASE:
|
||||
# coverage for 2 points:
|
||||
# 1. without room type, availability rules associated
|
||||
# with the pricelist are applied
|
||||
# 2. availability rule "closed" is taken into account
|
||||
|
||||
# ARRANGE
|
||||
self.create_common_scenario()
|
||||
self.test_room_type_availability1_item1 = self.env[
|
||||
"pms.room.type.availability.rule"
|
||||
].create(
|
||||
{
|
||||
"availability_id": self.test_room_type_availability1.id,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"date": (fields.datetime.today() + datetime.timedelta(days=2)).date(),
|
||||
"closed": True, # <- (1/2)
|
||||
}
|
||||
)
|
||||
# ACT
|
||||
result = self.env["pms.room.type.availability"].rooms_available(
|
||||
checkin=fields.date.today(),
|
||||
checkout=(fields.datetime.today() + datetime.timedelta(days=4)).date(),
|
||||
# room_type_id=False, # <- (2/2)
|
||||
pricelist=self.test_pricelist1.id,
|
||||
)
|
||||
# ASSERT
|
||||
self.assertNotIn(
|
||||
self.test_room_type_double,
|
||||
result.mapped("room_type_id"),
|
||||
"Availability should not contain rooms of a type "
|
||||
"which its availability rules applies",
|
||||
)
|
||||
|
||||
def test_availability_rules(self):
|
||||
# TEST CASE
|
||||
# the availability should take into acount availability rules:
|
||||
# closed_arrival, closed_departure, min_stay, max_stay,
|
||||
# min_stay_arrival, max_stay_arrival
|
||||
|
||||
# ARRANGE
|
||||
self.create_common_scenario()
|
||||
|
||||
self.test_room_type_availability1_item1 = self.env[
|
||||
"pms.room.type.availability.rule"
|
||||
].create(
|
||||
{
|
||||
"availability_id": self.test_room_type_availability1.id,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"date": (fields.datetime.today() + datetime.timedelta(days=0)).date(),
|
||||
}
|
||||
)
|
||||
|
||||
checkin = fields.date.today()
|
||||
checkout = (fields.datetime.today() + datetime.timedelta(days=4)).date()
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
"closed": False,
|
||||
"closed_arrival": True,
|
||||
"closed_departure": False,
|
||||
"min_stay": 0,
|
||||
"max_stay": 0,
|
||||
"min_stay_arrival": 0,
|
||||
"max_stay_arrival": 0,
|
||||
"quota": -1,
|
||||
"max_avail": -1,
|
||||
"date": checkin,
|
||||
},
|
||||
{
|
||||
"closed": False,
|
||||
"closed_arrival": False,
|
||||
"closed_departure": True,
|
||||
"min_stay": 0,
|
||||
"max_stay": 0,
|
||||
"min_stay_arrival": 0,
|
||||
"max_stay_arrival": 0,
|
||||
"quota": -1,
|
||||
"max_avail": -1,
|
||||
"date": checkout,
|
||||
},
|
||||
{
|
||||
"closed": False,
|
||||
"closed_arrival": False,
|
||||
"closed_departure": False,
|
||||
"min_stay": 5,
|
||||
"max_stay": 0,
|
||||
"min_stay_arrival": 0,
|
||||
"max_stay_arrival": 0,
|
||||
"quota": -1,
|
||||
"max_avail": -1,
|
||||
"date": checkin,
|
||||
},
|
||||
{
|
||||
"closed": False,
|
||||
"closed_arrival": False,
|
||||
"closed_departure": False,
|
||||
"min_stay": 0,
|
||||
"max_stay": 2,
|
||||
"min_stay_arrival": 0,
|
||||
"max_stay_arrival": 0,
|
||||
"quota": -1,
|
||||
"max_avail": -1,
|
||||
"date": checkin,
|
||||
},
|
||||
{
|
||||
"closed": False,
|
||||
"closed_arrival": False,
|
||||
"closed_departure": False,
|
||||
"min_stay": 0,
|
||||
"max_stay": 0,
|
||||
"min_stay_arrival": 5,
|
||||
"max_stay_arrival": 0,
|
||||
"quota": -1,
|
||||
"max_avail": -1,
|
||||
"date": checkin,
|
||||
},
|
||||
{
|
||||
"closed": False,
|
||||
"closed_arrival": False,
|
||||
"closed_departure": False,
|
||||
"min_stay": 0,
|
||||
"max_stay": 0,
|
||||
"min_stay_arrival": 0,
|
||||
"max_stay_arrival": 3,
|
||||
"quota": -1,
|
||||
"max_avail": -1,
|
||||
"date": checkin,
|
||||
},
|
||||
{
|
||||
"closed": False,
|
||||
"closed_arrival": False,
|
||||
"closed_departure": False,
|
||||
"min_stay": 0,
|
||||
"max_stay": 0,
|
||||
"min_stay_arrival": 0,
|
||||
"max_stay_arrival": 0,
|
||||
"quota": 0,
|
||||
"max_avail": -1,
|
||||
"date": checkin,
|
||||
},
|
||||
{
|
||||
"closed": False,
|
||||
"closed_arrival": False,
|
||||
"closed_departure": False,
|
||||
"min_stay": 0,
|
||||
"max_stay": 0,
|
||||
"min_stay_arrival": 0,
|
||||
"max_stay_arrival": 0,
|
||||
"quota": -1,
|
||||
"max_avail": 0,
|
||||
"date": checkin,
|
||||
},
|
||||
]
|
||||
|
||||
for test_case in test_cases:
|
||||
with self.subTest(k=test_case):
|
||||
|
||||
# ACT
|
||||
self.test_room_type_availability1_item1.write(test_case)
|
||||
|
||||
result = self.env["pms.room.type.availability"].rooms_available(
|
||||
checkin=checkin,
|
||||
checkout=checkout,
|
||||
room_type_id=self.test_room_type_double.id,
|
||||
pricelist=self.test_pricelist1.id,
|
||||
)
|
||||
|
||||
# ASSERT
|
||||
self.assertNotIn(
|
||||
self.test_room_type_double,
|
||||
result.mapped("room_type_id"),
|
||||
"Availability should not contain rooms of a type "
|
||||
"which its availability rules applies",
|
||||
)
|
||||
|
||||
@freeze_time("1980-11-01")
|
||||
def test_rule_on_create_reservation(self):
|
||||
# TEST CASE
|
||||
# an availability rule should be applied that would prevent the
|
||||
# creation of reservations
|
||||
|
||||
# ARRANGE
|
||||
self.create_common_scenario()
|
||||
self.test_room_type_availability1_item1 = self.env[
|
||||
"pms.room.type.availability.rule"
|
||||
].create(
|
||||
{
|
||||
"availability_id": self.test_room_type_availability1.id,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"date": (fields.datetime.today() + datetime.timedelta(days=2)).date(),
|
||||
"closed": True,
|
||||
}
|
||||
)
|
||||
|
||||
checkin = datetime.datetime.now()
|
||||
checkout = datetime.datetime.now() + datetime.timedelta(days=4)
|
||||
|
||||
# ACT & ASSERT
|
||||
with self.assertRaises(
|
||||
ValidationError,
|
||||
msg="Availability rules should be applied that would"
|
||||
" prevent the creation of the reservation.",
|
||||
):
|
||||
self.env["pms.reservation"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"checkin": checkin,
|
||||
"checkout": checkout,
|
||||
"adults": 2,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"pricelist_id": self.test_pricelist1.id,
|
||||
}
|
||||
)
|
||||
|
||||
@freeze_time("1980-11-01")
|
||||
def test_rules_on_create_splitted_reservation(self):
|
||||
# TEST CASE
|
||||
# an availability rule should be applied that would prevent the
|
||||
# creation of reservations including splitted reservations.
|
||||
|
||||
# ARRANGE
|
||||
self.create_common_scenario()
|
||||
self.test_room_type_availability1_item1 = self.env[
|
||||
"pms.room.type.availability.rule"
|
||||
].create(
|
||||
{
|
||||
"availability_id": self.test_room_type_availability1.id,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"date": (fields.datetime.today() + datetime.timedelta(days=2)).date(),
|
||||
"closed": True,
|
||||
}
|
||||
)
|
||||
|
||||
checkin_test = datetime.datetime.now()
|
||||
checkout_test = datetime.datetime.now() + datetime.timedelta(days=4)
|
||||
|
||||
self.env["pms.reservation"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"checkin": datetime.datetime.now(),
|
||||
"checkout": datetime.datetime.now() + datetime.timedelta(days=2),
|
||||
"adults": 2,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"preferred_room_id": self.test_room1_double.id,
|
||||
}
|
||||
)
|
||||
|
||||
self.env["pms.reservation"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"checkin": datetime.datetime.now() + datetime.timedelta(days=2),
|
||||
"checkout": datetime.datetime.now() + datetime.timedelta(days=4),
|
||||
"adults": 2,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"preferred_room_id": self.test_room2_double.id,
|
||||
}
|
||||
)
|
||||
|
||||
# ACT & ASSERT
|
||||
with self.assertRaises(
|
||||
ValidationError,
|
||||
msg="Availability rule should be applied that would"
|
||||
" prevent the creation of splitted reservation.",
|
||||
):
|
||||
self.env["pms.reservation"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"checkin": checkin_test,
|
||||
"checkout": checkout_test,
|
||||
"adults": 2,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"pricelist_id": self.test_pricelist1.id,
|
||||
}
|
||||
)
|
||||
|
||||
@freeze_time("1980-11-01")
|
||||
def test_rule_update_quota_on_create_reservation(self):
|
||||
# TEST CASE
|
||||
# quota rule is changed after creating a reservation
|
||||
# with pricelist linked to a availability plan that applies
|
||||
|
||||
# ARRANGE
|
||||
self.create_common_scenario()
|
||||
|
||||
self.test_room_type_availability1_item1 = self.env[
|
||||
"pms.room.type.availability.rule"
|
||||
].create(
|
||||
{
|
||||
"availability_id": self.test_room_type_availability1.id,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"date": datetime.date.today(),
|
||||
"quota": 1,
|
||||
}
|
||||
)
|
||||
r1 = self.env["pms.reservation"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"checkin": datetime.date.today(),
|
||||
"checkout": datetime.date.today() + datetime.timedelta(days=1),
|
||||
"adults": 2,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"pricelist_id": self.test_pricelist1.id,
|
||||
}
|
||||
)
|
||||
r1.flush()
|
||||
with self.assertRaises(
|
||||
ValidationError,
|
||||
msg="The quota shouldnt be enough to create a new reservation",
|
||||
):
|
||||
self.env["pms.reservation"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"checkin": datetime.date.today(),
|
||||
"checkout": datetime.date.today() + datetime.timedelta(days=1),
|
||||
"adults": 2,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"pricelist_id": self.test_pricelist1.id,
|
||||
}
|
||||
)
|
||||
|
||||
@freeze_time("1980-11-01")
|
||||
def test_rule_update_quota_on_update_reservation(self):
|
||||
# TEST CASE
|
||||
# quota rule is restored after creating a reservation
|
||||
# with pricelist linked to a availability rule that applies
|
||||
# and then modify the pricelist of the reservation and
|
||||
# no rules applies
|
||||
|
||||
# ARRANGE
|
||||
self.create_common_scenario()
|
||||
test_quota = 2
|
||||
test_pricelist2 = self.env["product.pricelist"].create(
|
||||
{
|
||||
"name": "test pricelist 2",
|
||||
}
|
||||
)
|
||||
rule = self.env["pms.room.type.availability.rule"].create(
|
||||
{
|
||||
"availability_id": self.test_room_type_availability1.id,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"date": datetime.date.today(),
|
||||
"quota": test_quota,
|
||||
}
|
||||
)
|
||||
reservation = self.env["pms.reservation"].create(
|
||||
{
|
||||
"pms_property_id": self.test_property.id,
|
||||
"checkin": datetime.date.today(),
|
||||
"checkout": datetime.date.today() + datetime.timedelta(days=1),
|
||||
"adults": 2,
|
||||
"room_type_id": self.test_room_type_double.id,
|
||||
"pricelist_id": self.test_pricelist1.id,
|
||||
}
|
||||
)
|
||||
|
||||
# ACT
|
||||
reservation.pricelist_id = test_pricelist2.id
|
||||
reservation.flush()
|
||||
self.assertEqual(
|
||||
test_quota,
|
||||
rule.quota,
|
||||
"The quota should be restored after changing the reservation's pricelist",
|
||||
)
|
||||
@@ -21,10 +21,10 @@
|
||||
<group
|
||||
colspan="4"
|
||||
col="4"
|
||||
string="Price and Restriction Plans"
|
||||
string="Price and Availability Plans"
|
||||
>
|
||||
<field name="default_pricelist_id" required="True" />
|
||||
<field name="default_restriction_id" required="True" />
|
||||
<field name="default_availability_id" required="True" />
|
||||
</group>
|
||||
<group string="Timezone">
|
||||
<field name="tz" widget="timezone_mismatch" />
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<record id="room_type_restriction_item_view_form" model="ir.ui.view">
|
||||
<field name="name">pms.room.type.restriction.item.form</field>
|
||||
<field name="model">pms.room.type.restriction.item</field>
|
||||
<record id="room_type_availability_rule_view_form" model="ir.ui.view">
|
||||
<field name="name">pms.room.type.availability.rule.form</field>
|
||||
<field name="model">pms.room.type.availability.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Restrictions">
|
||||
<form string="Availability Plans">
|
||||
<group>
|
||||
<field name="room_type_id" required="True" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="date" />
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="min_stay" />
|
||||
<field name="min_stay_arrival" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="max_stay" />
|
||||
<field name="max_stay_arrival" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="quota" />
|
||||
<field name="max_avail" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="closed" />
|
||||
@@ -26,11 +31,11 @@
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="room_type_restriction_item_view_tree" model="ir.ui.view">
|
||||
<field name="name">pms.room.type.restriction.item.tree</field>
|
||||
<field name="model">pms.room.type.restriction.item</field>
|
||||
<record id="room_type_availability_rule_view_tree" model="ir.ui.view">
|
||||
<field name="name">pms.room.type.availability.rule.tree</field>
|
||||
<field name="model">pms.room.type.availability.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Restrictions">
|
||||
<tree string="Availability rules">
|
||||
<field name="room_type_id" />
|
||||
<field name="date" />
|
||||
<field name="min_stay" />
|
||||
81
pms/views/pms_room_type_availability_views.xml
Normal file
81
pms/views/pms_room_type_availability_views.xml
Normal file
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<record id="room_type_availability_view_form" model="ir.ui.view">
|
||||
<field name="name">pms.room.type.availability.form</field>
|
||||
<field name="model">pms.room.type.availability</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Rules">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
name="toggle_active"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-archive"
|
||||
>
|
||||
<field
|
||||
name="active"
|
||||
widget="boolean_button"
|
||||
options='{"terminology": "archive"}'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name" string="Name" />
|
||||
<h1>
|
||||
<field name="name" />
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<field
|
||||
name="pms_property_id"
|
||||
options="{'no_create': True,'no_open': True}"
|
||||
/>
|
||||
<field
|
||||
name="pms_pricelist_ids"
|
||||
widget="many2many_tags"
|
||||
options="{'no_create': True,'no_open': True}"
|
||||
/>
|
||||
</group>
|
||||
<separator string="Availability Rules" />
|
||||
<field name="item_ids" nolabel="1">
|
||||
<tree string="Availability Rules">
|
||||
<field name="room_type_id" />
|
||||
<field name="date" />
|
||||
</tree>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="room_type_availability_view_tree" model="ir.ui.view">
|
||||
<field name="name">pms.room.type.availability.tree</field>
|
||||
<field name="model">pms.room.type.availability</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Availability Plans">
|
||||
<field name="name" />
|
||||
<field name="pms_property_id" />
|
||||
<field
|
||||
name="pms_pricelist_ids"
|
||||
widget="many2many_tags"
|
||||
options="{'no_create': True,'no_open': True}"
|
||||
/>
|
||||
<field name="active" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Action of reservation availability plan-->
|
||||
<record model="ir.actions.act_window" id="room_type_availability_action">
|
||||
<field name="name">Reservation Availability Plans</field>
|
||||
<field name="res_model">pms.room.type.availability</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<!-- MENUS -->
|
||||
<menuitem
|
||||
name="Availability Plans"
|
||||
id="reservation_availability_rules_menu"
|
||||
action="room_type_availability_action"
|
||||
sequence="22"
|
||||
parent="pms.configuration_others"
|
||||
/>
|
||||
</odoo>
|
||||
@@ -1,72 +0,0 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<record id="room_type_restriction_view_form" model="ir.ui.view">
|
||||
<field name="name">pms.room.type.restriction.form</field>
|
||||
<field name="model">pms.room.type.restriction</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Restrictions">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
name="toggle_active"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-archive"
|
||||
>
|
||||
<field
|
||||
name="active"
|
||||
widget="boolean_button"
|
||||
options='{"terminology": "archive"}'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name" string="Name" />
|
||||
<h1>
|
||||
<field name="name" />
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<field
|
||||
name="pms_property_id"
|
||||
options="{'no_create': True,'no_open': True}"
|
||||
/>
|
||||
<separator string="Restriction Items" />
|
||||
<field name="item_ids" nolabel="1">
|
||||
<tree string="Restriction Items">
|
||||
<field name="room_type_id" />
|
||||
<field name="date" />
|
||||
<field name="min_stay" />
|
||||
<field name="closed" />
|
||||
</tree>
|
||||
</field>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="room_type_restriction_view_tree" model="ir.ui.view">
|
||||
<field name="name">pms.room.type.restriction.tree</field>
|
||||
<field name="model">pms.room.type.restriction</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Restrictions">
|
||||
<field name="name" />
|
||||
<field name="active" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Action of reservation restriction -->
|
||||
<record model="ir.actions.act_window" id="room_type_restriction_action">
|
||||
<field name="name">Reservation restrictions</field>
|
||||
<field name="res_model">pms.room.type.restriction</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<!-- MENUS -->
|
||||
<menuitem
|
||||
name="Restrictions"
|
||||
id="reservation_restriction_menu"
|
||||
action="room_type_restriction_action"
|
||||
sequence="22"
|
||||
parent="pms.configuration_others"
|
||||
/>
|
||||
</odoo>
|
||||
@@ -12,6 +12,8 @@
|
||||
/>
|
||||
<field name="pricelist_type" />
|
||||
<field name="cancelation_rule_id" />
|
||||
<field name="availability_id" />
|
||||
|
||||
</xpath>
|
||||
<xpath
|
||||
expr="//field[@name='item_ids']/tree/field[@name='base']"
|
||||
|
||||
Reference in New Issue
Block a user