mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
@@ -9,6 +9,7 @@ import pytz
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
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
|
||||
@@ -622,3 +623,77 @@ class PmsProperty(models.Model):
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
from odoo import fields, models
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class ResCountryState(models.Model):
|
||||
_inherit = "res.country.state"
|
||||
|
||||
ine_code = fields.Char(string="INE State Code")
|
||||
|
||||
@api.constrains("ine_code")
|
||||
def _check_ine_code(self):
|
||||
for record in self:
|
||||
if record.country_id.code == "ES" and not record.ine_code:
|
||||
raise models.ValidationError(
|
||||
_(
|
||||
"The state %s of %s must have an INE code"
|
||||
% (record.name, record.country_id.name)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -753,12 +753,19 @@ class TestWizardINE(TestPms):
|
||||
# ARRANGE
|
||||
self.ideal_scenario()
|
||||
start_date = datetime.date(2021, 2, 1)
|
||||
end_date = datetime.date(2021, 2, 28)
|
||||
expected_monthly_adr = 23.58
|
||||
|
||||
# ACT
|
||||
monthly_adr = self.env["pms.ine.wizard"].ine_calculate_monthly_adr(
|
||||
start_date, self.pms_property1.id
|
||||
wizard = self.env["pms.ine.wizard"].new(
|
||||
{
|
||||
"pms_property_id": self.pms_property1.id,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
}
|
||||
)
|
||||
|
||||
monthly_adr = wizard.ine_calculate_adr(start_date, end_date)
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_monthly_adr,
|
||||
@@ -777,19 +784,25 @@ class TestWizardINE(TestPms):
|
||||
+----------------+-------+-------+-------+
|
||||
| monthly revpar | 23.58 |
|
||||
+----------------+-------+-------+-------+
|
||||
num rooms avail. = 5
|
||||
num rooms avail = 5
|
||||
income = 25.00 + 21.00 + 25.00 + 25.00 + 21.50 + 21.50 = 139
|
||||
monthly revpar = 139 / (5 * 28) = 0.99
|
||||
"""
|
||||
# ARRANGE
|
||||
self.ideal_scenario()
|
||||
start_date = datetime.date(2021, 2, 1)
|
||||
end_date = datetime.date(2021, 2, 28)
|
||||
expected_monthly_revpar = 0.99
|
||||
|
||||
# ACT
|
||||
monthly_revpar = self.env["pms.ine.wizard"].ine_calculate_monthly_revpar(
|
||||
start_date, self.pms_property1.id
|
||||
wizard = self.env["pms.ine.wizard"].new(
|
||||
{
|
||||
"pms_property_id": self.pms_property1.id,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
}
|
||||
)
|
||||
monthly_revpar = wizard.ine_calculate_revpar(start_date, end_date)
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_monthly_revpar,
|
||||
@@ -898,7 +911,7 @@ class TestWizardINE(TestPms):
|
||||
"""
|
||||
# ARRANGE
|
||||
self.ideal_scenario()
|
||||
self.partner_2.nationality_id = False
|
||||
self.reservation_1.checkin_partner_ids[1].nationality_id = False
|
||||
start_date = datetime.date(2021, 2, 1)
|
||||
end_date = datetime.date(2021, 2, 4)
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ class WizardIne(models.TransientModel):
|
||||
required=True,
|
||||
)
|
||||
|
||||
adr = fields.Float(string="Monthly ADR")
|
||||
revpar = fields.Float(string="Monthly RevPAR")
|
||||
adr = fields.Float(string="Range ADR")
|
||||
revpar = fields.Float(string="Range RevPAR")
|
||||
|
||||
@api.model
|
||||
def ine_rooms(self, start_date, end_date, pms_property_id):
|
||||
@@ -198,7 +198,7 @@ class WizardIne(models.TransientModel):
|
||||
|
||||
for entry in read_group_result:
|
||||
if not entry["nationality_id"]:
|
||||
guests_with_no_nationality = self.env["res.partner"].search(
|
||||
guests_with_no_nationality = self.env["pms.checkin.partner"].search(
|
||||
entry["__domain"]
|
||||
)
|
||||
guests_with_no_nationality = (
|
||||
@@ -232,7 +232,7 @@ class WizardIne(models.TransientModel):
|
||||
nationalities[nationality_id_code][date][type_of_entry] = num
|
||||
else:
|
||||
# arrivals grouped by state_id (Spain "provincias")
|
||||
read_by_arrivals_spain = self.env["res.partner"].read_group(
|
||||
read_by_arrivals_spain = self.env["pms.checkin.partner"].read_group(
|
||||
entry["__domain"],
|
||||
["residence_state_id"],
|
||||
["residence_state_id"],
|
||||
@@ -242,7 +242,7 @@ class WizardIne(models.TransientModel):
|
||||
for entry_from_spain in read_by_arrivals_spain:
|
||||
if not entry_from_spain["residence_state_id"]:
|
||||
spanish_guests_with_no_state = self.env[
|
||||
"res.partner"
|
||||
"pms.checkin.partner"
|
||||
].search(entry_from_spain["__domain"])
|
||||
spanish_guests_with_no_state = (
|
||||
str(spanish_guests_with_no_state.mapped("name"))
|
||||
@@ -352,8 +352,8 @@ class WizardIne(models.TransientModel):
|
||||
arrivals = hosts.filtered(lambda x: x.checkin == p_date)
|
||||
|
||||
# arrivals grouped by nationality_id
|
||||
read_by_arrivals = self.env["res.partner"].read_group(
|
||||
[("id", "in", arrivals.mapped("partner_id").ids)],
|
||||
read_by_arrivals = self.env["pms.checkin.partner"].read_group(
|
||||
[("id", "in", arrivals.ids)],
|
||||
["nationality_id"],
|
||||
["nationality_id"],
|
||||
orderby="nationality_id",
|
||||
@@ -364,8 +364,8 @@ class WizardIne(models.TransientModel):
|
||||
departures = hosts.filtered(lambda x: x.checkout == p_date)
|
||||
|
||||
# departures grouped by nationality_id
|
||||
read_by_departures = self.env["res.partner"].read_group(
|
||||
[("id", "in", departures.mapped("partner_id").ids)],
|
||||
read_by_departures = self.env["pms.checkin.partner"].read_group(
|
||||
[("id", "in", departures.ids)],
|
||||
["nationality_id"],
|
||||
["nationality_id"],
|
||||
orderby="nationality_id",
|
||||
@@ -376,14 +376,13 @@ class WizardIne(models.TransientModel):
|
||||
pernoctations = hosts - departures
|
||||
|
||||
# pernoctations grouped by nationality_id
|
||||
read_by_pernoctations = self.env["res.partner"].read_group(
|
||||
[("id", "in", pernoctations.mapped("partner_id").ids)],
|
||||
read_by_pernoctations = self.env["pms.checkin.partner"].read_group(
|
||||
[("id", "in", pernoctations.ids)],
|
||||
["nationality_id"],
|
||||
["nationality_id"],
|
||||
orderby="nationality_id",
|
||||
lazy=False,
|
||||
)
|
||||
|
||||
ine_add_arrivals_departures_pernoctations(
|
||||
p_date, "arrivals", read_by_arrivals
|
||||
)
|
||||
@@ -409,73 +408,59 @@ class WizardIne(models.TransientModel):
|
||||
partners_to_unlink.unlink()
|
||||
return nationalities
|
||||
|
||||
@api.model
|
||||
def ine_calculate_monthly_adr(self, start_date, pms_property_id):
|
||||
month = start_date.month
|
||||
year = start_date.year
|
||||
month_range = calendar.monthrange(start_date.year, start_date.month)
|
||||
first_day = datetime.date(year, month, 1)
|
||||
last_day = datetime.date(year, month, month_range[1])
|
||||
group_adr = self.env["pms.reservation.line"].read_group(
|
||||
[
|
||||
("pms_property_id", "=", pms_property_id),
|
||||
("occupies_availability", "=", True),
|
||||
("reservation_id.reservation_type", "=", "normal"),
|
||||
("room_id.in_ine", "=", True),
|
||||
("date", ">=", first_day),
|
||||
("date", "<=", last_day),
|
||||
],
|
||||
["price:avg"],
|
||||
["date:day"],
|
||||
)
|
||||
if not len(group_adr):
|
||||
return 0
|
||||
adr = 0
|
||||
for day_adr in group_adr:
|
||||
adr += day_adr["price"]
|
||||
|
||||
adr = round(adr / len(group_adr), 2)
|
||||
def ine_calculate_adr(self, start_date, end_date, domain=False):
|
||||
"""
|
||||
Calculate date range ADR 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.append(("room_id.in_ine", "=", True))
|
||||
adr = self.pms_property_id._get_adr(start_date, end_date, domain)
|
||||
self.adr = adr
|
||||
return adr
|
||||
|
||||
@api.model
|
||||
def ine_calculate_monthly_revpar(self, start_date, pms_property_id):
|
||||
month = start_date.month
|
||||
year = start_date.year
|
||||
month_range = calendar.monthrange(start_date.year, start_date.month)
|
||||
first_day = datetime.date(year, month, 1)
|
||||
last_day = datetime.date(year, month, month_range[1])
|
||||
sum_group_price = self.env["pms.reservation.line"].read_group(
|
||||
[
|
||||
("pms_property_id", "=", pms_property_id),
|
||||
("occupies_availability", "=", True),
|
||||
("reservation_id.reservation_type", "=", "normal"),
|
||||
("room_id.in_ine", "=", True),
|
||||
("date", ">=", first_day),
|
||||
("date", "<=", last_day),
|
||||
],
|
||||
["price"],
|
||||
[],
|
||||
)
|
||||
count_room_days_not_allowed = len(
|
||||
self.env["pms.reservation.line"].search(
|
||||
[
|
||||
("pms_property_id", "=", pms_property_id),
|
||||
("occupies_availability", "=", True),
|
||||
("reservation_id.reservation_type", "!=", "normal"),
|
||||
("date", ">=", first_day),
|
||||
("date", "<=", last_day),
|
||||
]
|
||||
)
|
||||
)
|
||||
pms_property = self.env["pms.property"].browse(pms_property_id)
|
||||
count_total_room_days = len(pms_property.room_ids) * month_range[1]
|
||||
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)
|
||||
def ine_calculate_revpar(self, start_date, end_date, domain=False):
|
||||
"""
|
||||
Calculate date range 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.append(("room_id.in_ine", "=", True))
|
||||
revpar = self.pms_property_id._get_revpar(start_date, end_date, domain)
|
||||
self.revpar = revpar
|
||||
return revpar
|
||||
|
||||
def ine_calculate_occupancy(self, start_date, end_date, domain=False):
|
||||
"""
|
||||
Calculate date range occupancy 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
|
||||
total_domain = [
|
||||
("room_id.in_ine", "=", True),
|
||||
("date", ">=", start_date),
|
||||
("date", "<=", end_date),
|
||||
]
|
||||
total_reservations = self.env["pms.reservation.line"].search(total_domain)
|
||||
domain.extend(total_domain)
|
||||
filter_reservations = self.env["pms.reservation.line"].search(domain)
|
||||
if len(filter_reservations) > 0:
|
||||
filter_percent = round(
|
||||
len(filter_reservations) * 100 / len(total_reservations), 2
|
||||
)
|
||||
else:
|
||||
filter_percent = 0
|
||||
return filter_percent
|
||||
|
||||
@api.model
|
||||
def ine_get_nif_cif(self, cif_nif):
|
||||
country_codes = self.env["res.country"].search([]).mapped("code")
|
||||
@@ -525,9 +510,6 @@ class WizardIne(models.TransientModel):
|
||||
|
||||
self.check_ine_mandatory_fields(self.pms_property_id)
|
||||
|
||||
if self.start_date.month != self.end_date.month:
|
||||
raise ValidationError(_("The date range must belong to the same month."))
|
||||
|
||||
number_of_rooms = sum(
|
||||
self.env["pms.room"]
|
||||
.search(
|
||||
@@ -688,16 +670,16 @@ class WizardIne(models.TransientModel):
|
||||
prices_tag = ET.SubElement(survey_tag, "PRECIOS")
|
||||
|
||||
ET.SubElement(prices_tag, "REVPAR_MENSUAL").text = str(
|
||||
self.ine_calculate_monthly_revpar(
|
||||
self.ine_calculate_revpar(
|
||||
self.start_date,
|
||||
self.pms_property_id.id,
|
||||
self.end_date,
|
||||
)
|
||||
)
|
||||
|
||||
ET.SubElement(prices_tag, "ADR_MENSUAL").text = str(
|
||||
self.ine_calculate_monthly_adr(
|
||||
self.ine_calculate_adr(
|
||||
self.start_date,
|
||||
self.pms_property_id.id,
|
||||
self.end_date,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -715,24 +697,126 @@ class WizardIne(models.TransientModel):
|
||||
ET.SubElement(
|
||||
prices_tag, "PCTN_HABITACIONES_OCUPADAS_TOUROPERADOR_ONLINE"
|
||||
).text = "0"
|
||||
ET.SubElement(prices_tag, "ADR_EMPRESAS").text = "0"
|
||||
ET.SubElement(prices_tag, "PCTN_HABITACIONES_OCUPADAS_EMPRESAS").text = "0"
|
||||
ET.SubElement(prices_tag, "ADR_AGENCIA_DE_VIAJE_TRADICIONAL").text = "0"
|
||||
ET.SubElement(prices_tag, "ADR_EMPRESAS").text = str(
|
||||
self.ine_calculate_adr(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[
|
||||
("reservation_id.partner_id", "!=", False),
|
||||
("reservation_id.partner_id.is_company", "=", True),
|
||||
],
|
||||
)
|
||||
)
|
||||
ET.SubElement(prices_tag, "PCTN_HABITACIONES_OCUPADAS_EMPRESAS").text = str(
|
||||
self.ine_calculate_occupancy(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[
|
||||
("reservation_id.partner_id", "!=", False),
|
||||
("reservation_id.partner_id.is_company", "=", True),
|
||||
],
|
||||
)
|
||||
)
|
||||
ET.SubElement(prices_tag, "ADR_AGENCIA_DE_VIAJE_TRADICIONAL").text = str(
|
||||
self.ine_calculate_adr(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[
|
||||
("reservation_id.agency_id", "!=", False),
|
||||
("reservation_id.agency_id.sale_channel_id.is_on_line", "=", False),
|
||||
],
|
||||
)
|
||||
)
|
||||
ET.SubElement(
|
||||
prices_tag, "PCTN_HABITACIONES_OCUPADAS_AGENCIA_TRADICIONAL"
|
||||
).text = "0"
|
||||
ET.SubElement(prices_tag, "ADR_AGENCIA_DE_VIAJE_ONLINE").text = "0"
|
||||
).text = str(
|
||||
self.ine_calculate_occupancy(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[
|
||||
("reservation_id.agency_id", "!=", False),
|
||||
("reservation_id.agency_id.sale_channel_id.is_on_line", "=", False),
|
||||
],
|
||||
)
|
||||
)
|
||||
ET.SubElement(prices_tag, "ADR_AGENCIA_DE_VIAJE_ONLINE").text = str(
|
||||
self.ine_calculate_adr(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[
|
||||
("reservation_id.agency_id", "!=", False),
|
||||
("reservation_id.agency_id.sale_channel_id.is_on_line", "=", True),
|
||||
],
|
||||
)
|
||||
)
|
||||
ET.SubElement(
|
||||
prices_tag, "PCTN_HABITACIONES_OCUPADAS_AGENCIA_ONLINE"
|
||||
).text = "0"
|
||||
ET.SubElement(prices_tag, "ADR_PARTICULARES").text = "0"
|
||||
ET.SubElement(prices_tag, "PCTN_HABITACIONES_OCUPADAS_PARTICULARES").text = "0"
|
||||
).text = str(
|
||||
self.ine_calculate_occupancy(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[
|
||||
("reservation_id.agency_id", "!=", False),
|
||||
("reservation_id.agency_id.sale_channel_id.is_on_line", "=", True),
|
||||
],
|
||||
)
|
||||
)
|
||||
ET.SubElement(prices_tag, "ADR_PARTICULARES").text = str(
|
||||
self.ine_calculate_adr(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[
|
||||
"|",
|
||||
("reservation_id.partner_id", "=", False),
|
||||
("reservation_id.partner_id.is_company", "!=", False),
|
||||
],
|
||||
)
|
||||
)
|
||||
ET.SubElement(prices_tag, "PCTN_HABITACIONES_OCUPADAS_PARTICULARES").text = str(
|
||||
self.ine_calculate_occupancy(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[
|
||||
"|",
|
||||
("reservation_id.partner_id", "=", False),
|
||||
("reservation_id.partner_id.is_company", "=", False),
|
||||
],
|
||||
)
|
||||
)
|
||||
ET.SubElement(prices_tag, "ADR_GRUPOS").text = "0"
|
||||
ET.SubElement(prices_tag, "PCTN_HABITACIONES_OCUPADAS_GRUPOS").text = "0"
|
||||
ET.SubElement(prices_tag, "ADR_INTERNET").text = "0"
|
||||
ET.SubElement(prices_tag, "PCTN_HABITACIONES_OCUPADAS_INTERNET").text = "0"
|
||||
ET.SubElement(prices_tag, "ADR_OTROS").text = "0"
|
||||
ET.SubElement(prices_tag, "PCTN_HABITACIONES_OCUPADAS_OTROS").text = "0"
|
||||
ET.SubElement(prices_tag, "ADR_INTERNET").text = str(
|
||||
self.ine_calculate_adr(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[("reservation_id.channel_type_id.is_on_line", "=", True)],
|
||||
)
|
||||
)
|
||||
ET.SubElement(prices_tag, "PCTN_HABITACIONES_OCUPADAS_INTERNET").text = str(
|
||||
self.ine_calculate_occupancy(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[("reservation_id.channel_type_id.is_on_line", "=", True)],
|
||||
)
|
||||
)
|
||||
ET.SubElement(prices_tag, "ADR_OTROS").text = str(
|
||||
self.ine_calculate_adr(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[("reservation_id.channel_type_id.is_on_line", "!=", True)],
|
||||
)
|
||||
)
|
||||
ET.SubElement(prices_tag, "PCTN_HABITACIONES_OCUPADAS_OTROS").text = str(
|
||||
self.ine_calculate_occupancy(
|
||||
self.start_date,
|
||||
self.end_date,
|
||||
[
|
||||
"|",
|
||||
("reservation_id.channel_type_id.is_on_line", "!=", True),
|
||||
("reservation_id.channel_type_id", "=", False),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
staff_tag = ET.SubElement(survey_tag, "PERSONAL_OCUPADO")
|
||||
ET.SubElement(staff_tag, "PERSONAL_NO_REMUNERADO").text = str(
|
||||
|
||||
Reference in New Issue
Block a user