diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index 265328e37..1e72a055f 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -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 diff --git a/pms_l10n_es/models/res_country_state.py b/pms_l10n_es/models/res_country_state.py index 67b0ecb39..108cb0c61 100644 --- a/pms_l10n_es/models/res_country_state.py +++ b/pms_l10n_es/models/res_country_state.py @@ -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) + ) + ) diff --git a/pms_l10n_es/tests/test_wizard_ine.py b/pms_l10n_es/tests/test_wizard_ine.py index 9f37c2c21..e929c5046 100644 --- a/pms_l10n_es/tests/test_wizard_ine.py +++ b/pms_l10n_es/tests/test_wizard_ine.py @@ -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) diff --git a/pms_l10n_es/wizards/wizard_ine.py b/pms_l10n_es/wizards/wizard_ine.py index 776441103..008b22a93 100644 --- a/pms_l10n_es/wizards/wizard_ine.py +++ b/pms_l10n_es/wizards/wizard_ine.py @@ -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(