Merge PR #35 into 14.0

Signed-off-by DarioLodeiros
This commit is contained in:
OCA-git-bot
2021-07-31 08:10:07 +00:00
27 changed files with 1818 additions and 57 deletions

View File

@@ -1,3 +1,4 @@
partner-contact
reporting-engine
queue
community-data-files

View File

@@ -342,7 +342,6 @@ class PmsReservation(models.Model):
reservation_type = fields.Selection(
string="Reservation Type",
help="Type of reservations. It can be 'normal', 'staff' or 'out of service",
default=lambda *a: "normal",
related="folio_id.reservation_type",
store=True,
readonly=False,
@@ -605,6 +604,29 @@ class PmsReservation(models.Model):
readonly=False,
)
check_adults = fields.Boolean(
help="Internal field to force room capacity validations",
compute="_compute_check_adults",
readonly=False,
store=True,
)
def _compute_date_order(self):
for record in self:
record.date_order = datetime.datetime.today()
@api.depends(
"service_ids",
"service_ids.service_line_ids",
"service_ids.service_line_ids.product_id",
"service_ids.service_line_ids.day_qty",
"reservation_line_ids",
"reservation_line_ids.room_id",
)
def _compute_check_adults(self):
for record in self:
record.check_adults = True
@api.depends(
"checkin",
"checkout",
@@ -1423,23 +1445,6 @@ class PmsReservation(models.Model):
# _("The room already is completed (%s)", record.name)
# )
@api.constrains("adults")
def _check_adults(self):
for record in self:
extra_bed = record.service_ids.filtered(
lambda r: r.product_id.is_extra_bed is True
)
for room in record.reservation_line_ids.room_id:
if record.adults + record.children_occupying > room.get_capacity(
sum(extra_bed.mapped("product_qty"))
):
raise ValidationError(
_(
"Persons can't be higher than room capacity (%s)",
record.name,
)
)
@api.constrains("state")
def _check_onboard_reservation(self):
for record in self:
@@ -1484,6 +1489,13 @@ class PmsReservation(models.Model):
if record.agency_id and not record.agency_id.is_agency:
raise ValidationError(_("booking agency with wrong configuration: "))
@api.constrains("check_adults")
def _check_capacity(self):
for record in self:
self.env["pms.room"]._check_adults(
record, record.service_ids.service_line_ids
)
# Action methods
def open_partner(self):
""" Utility method used to add an "View Customer" button in reservation views """
@@ -1623,6 +1635,12 @@ class PmsReservation(models.Model):
)
pms_property = self.env["pms.property"].browse(pms_property_id)
vals["name"] = pms_property.reservation_sequence_id._next_do()
if not vals.get("reservation_type"):
vals["reservation_type"] = (
folio.reservation_type if folio.reservation_type else "normal"
)
record = super(PmsReservation, self).create(vals)
if record.preconfirm:
record.confirm()

View File

@@ -470,17 +470,3 @@ class PmsReservationLine(models.Model):
)
if duplicated:
raise ValidationError(_("Duplicated reservation line date"))
@api.constrains("room_id")
def _check_adults(self):
for record in self.filtered("room_id"):
extra_bed = record.reservation_id.service_ids.filtered(
lambda r: r.product_id.is_extra_bed is True
)
if (
record.reservation_id.adults + record.reservation_id.children_occupying
> record.room_id.get_capacity(len(extra_bed))
):
raise ValidationError(_("Persons can't be higher than room capacity"))
# if record.reservation_id.adults == 0:
# raise ValidationError(_("Reservation has no adults"))

View File

@@ -118,6 +118,26 @@ class PmsRoom(models.Model):
)
)
@api.model
def _check_adults(self, reservation, service_line_ids=False):
for line in reservation.reservation_line_ids:
num_extra_beds = 0
if service_line_ids:
extra_beds = service_line_ids.filtered(
lambda x: x.date == line.date and x.product_id.is_extra_bed is True
)
num_extra_beds = sum(extra_beds.mapped("day_qty")) if extra_beds else 0
if line.room_id:
if (
reservation.adults + reservation.children_occupying
) > line.room_id.get_capacity(num_extra_beds):
raise ValidationError(
_(
"Persons can't be higher than room capacity (%s)",
reservation.name,
)
)
# Business methods
def get_capacity(self, extra_bed=0):

View File

@@ -340,23 +340,24 @@ class PmsService(models.Model):
move_day = 0
if consumed_on == "after":
move_day = 1
service.service_line_ids -= (
service.service_line_ids.filtered_domain(
[
"|",
(
"date",
"<",
reservation.checkin + timedelta(move_day),
),
(
"date",
">=",
reservation.checkout + timedelta(move_day),
),
]
)
)
for del_service_id in service.service_line_ids.filtered_domain(
[
"|",
(
"date",
"<",
reservation.checkin + timedelta(move_day),
),
(
"date",
">=",
reservation.checkout + timedelta(move_day),
),
]
).ids:
lines.append((2, del_service_id))
# TODO: check intermediate states in check_adults restriction
# when lines are removed
service.service_line_ids = lines
else:
if not service.service_line_ids:
@@ -535,10 +536,11 @@ class PmsService(models.Model):
# Businness Methods
def _service_day_qty(self):
self.ensure_one()
qty = self.product_qty if len(self.service_line_ids) == 1 else 0
qty = self.product_qty if len(self.service_line_ids) == 1 else 1
if not self.reservation_id:
return qty
# TODO: Pass per_person to service line from product default_per_person
# When the user modifies the quantity avoid overwriting
if self.product_id.per_person:
qty = self.reservation_id.adults
return qty

View File

@@ -36,6 +36,7 @@ class TestPmsReservations(TestPms):
"name": "Double 101",
"room_type_id": self.room_type_double.id,
"capacity": 2,
"extra_beds_allowed": 1,
}
)
@@ -626,19 +627,20 @@ class TestPmsReservations(TestPms):
# ACT & ASSERT
with self.assertRaises(
ValidationError,
msg="The number of people is lower than the capacity of the room",
msg="The number of people is greater than the capacity of the room",
):
self.env["pms.reservation"].create(
reservation = self.env["pms.reservation"].create(
{
"adults": 2,
"children_occupying": 1,
"checkin": datetime.datetime.now(),
"checkout": datetime.datetime.now() + datetime.timedelta(days=1),
"room_type_id": self.room_type_double.id,
"preferred_room_id": self.room1.id,
"partner_id": self.partner1.id,
"pms_property_id": self.pms_property1.id,
}
)
reservation.flush()
def test_to_assign_priority_reservation(self):
"""
@@ -1930,16 +1932,17 @@ class TestPmsReservations(TestPms):
}
)
with self.assertRaises(ValidationError):
self.env["pms.reservation"].create(
reservation = self.env["pms.reservation"].create(
{
"checkin": fields.date.today(),
"checkout": fields.date.today() + datetime.timedelta(days=3),
"pms_property_id": self.pms_property1.id,
"partner_id": self.host1.id,
"room_type_id": self.room_type_double.id,
"preferred_room_id": self.room1.id,
"adults": 4,
}
)
reservation.flush()
def test_check_format_arrival_hour(self):
"""
@@ -2616,7 +2619,7 @@ class TestPmsReservations(TestPms):
{
"checkin": fields.date.today() + datetime.timedelta(days=-3),
"checkout": fields.date.today() + datetime.timedelta(days=3),
"room_type_id": self.room_type_double.id,
"preferred_room_id": self.room1.id,
"partner_id": self.partner1.id,
"pms_property_id": self.pms_property1.id,
"pricelist_id": self.pricelist1.id,

View File

@@ -14,7 +14,46 @@
<notebook>
<page string="General Information" name="property_general">
<group>
<div class="o_address_format">
<field
name="street"
placeholder="Street..."
class="o_address_street"
/>
<field
name="street2"
placeholder="Street 2..."
class="o_address_street"
/>
<field
name="city"
placeholder="City"
class="o_address_city"
/>
<field
name="state_id"
class="o_address_state"
placeholder="State"
/>
<field
name="zip"
placeholder="ZIP"
class="o_address_zip"
/>
<field
name="country_id"
placeholder="Country"
class="o_address_country"
/>
</div>
</group>
<group>
<div class="row">
<div class="col-6">
<label for="phone" />
<field name="phone" />
</div>
</div>
</group>
</page>
<page string="Settings" name="property_settings">

View File

@@ -16,6 +16,7 @@
<field name="tax_ids" invisible="1" />
<field name="checkin_partner_count" invisible="1" />
<field name="to_assign" invisible="1" />
<field name="check_adults" invisible="1" />
<button
name="confirm"
string="Confirm"

View File

@@ -16,22 +16,29 @@
"partner_contact_gender",
"partner_contact_birthdate",
"partner_contact_nationality",
"base_iso3166",
"queue_job",
],
"external_dependencies": {
"python": [
"bs4",
"pycountry",
],
},
"data": [
"data/res.country.state.csv",
"data/pms.ine.tourism.type.category.csv",
"data/cron_jobs.xml",
"data/pms_data.xml",
"data/queue_data.xml",
"data/queue_job_function_data.xml",
"security/ir.model.access.csv",
"views/pms_property_views.xml",
"views/pms_room_views.xml",
"views/pms_log_institution_traveller_report_views.xml",
"views/pms_ine_tourism_type_category.xml",
"wizards/traveller_report.xml",
"wizards/wizard_ine.xml",
],
"installable": True,
}

View File

@@ -0,0 +1,53 @@
"type","category"
Hoteles,H1
Hoteles,H2
Hoteles,H3
Hoteles,H4
Hoteles,H5
Hoteles-apartamentos,AP
Hoteles-apartamentos,HA
Hoteles-apartamentos,HA1
Hoteles-apartamentos,HA2
Hoteles-apartamentos,HA3
Hoteles-apartamentos,HA4
Hoteles-apartamentos,HA5
Hoteles-residencias,HR
Hoteles-residencias,HR1
Hoteles-residencias,HR2
Hoteles-residencias,HR3
Hoteles-residencias,HR4
Hoteles-residencias,HR5
Moteles,M1
Moteles,M2
Moteles,M3
Paradores Nacionales,PN3
Paradores Nacionales,PN4
Paradores Nacionales,PN5
Ciudades de vacaciones,CV1
Ciudades de vacaciones,CV2
Ciudades de vacaciones,CV3
Residencias-apartamentos,RA1
Residencias-apartamentos,RA2
Residencias-apartamentos,RA3
Residencias-apartamentos,RA4
Hostales,HS
Hostales,HS1
Hostales,HS2
Hostales,HS3
Hostales,HSR
Hostales,HSR1
Hostales,HSR2
Hostales,HSR3
Hostales generales, HSE
Casas de Huéspedes,CH
Casas de Huéspedes,CH1
Fondas,F1
Fondas,F2
Fondas,F3
Pensiones,P
Pensiones,P1
Pensiones,P2
Pensiones,P3
Pensiones,PA
Pensiones,PT
Otros,Otras
1 type category
2 Hoteles H1
3 Hoteles H2
4 Hoteles H3
5 Hoteles H4
6 Hoteles H5
7 Hoteles-apartamentos AP
8 Hoteles-apartamentos HA
9 Hoteles-apartamentos HA1
10 Hoteles-apartamentos HA2
11 Hoteles-apartamentos HA3
12 Hoteles-apartamentos HA4
13 Hoteles-apartamentos HA5
14 Hoteles-residencias HR
15 Hoteles-residencias HR1
16 Hoteles-residencias HR2
17 Hoteles-residencias HR3
18 Hoteles-residencias HR4
19 Hoteles-residencias HR5
20 Moteles M1
21 Moteles M2
22 Moteles M3
23 Paradores Nacionales PN3
24 Paradores Nacionales PN4
25 Paradores Nacionales PN5
26 Ciudades de vacaciones CV1
27 Ciudades de vacaciones CV2
28 Ciudades de vacaciones CV3
29 Residencias-apartamentos RA1
30 Residencias-apartamentos RA2
31 Residencias-apartamentos RA3
32 Residencias-apartamentos RA4
33 Hostales HS
34 Hostales HS1
35 Hostales HS2
36 Hostales HS3
37 Hostales HSR
38 Hostales HSR1
39 Hostales HSR2
40 Hostales HSR3
41 Hostales generales HSE
42 Casas de Huéspedes CH
43 Casas de Huéspedes CH1
44 Fondas F1
45 Fondas F2
46 Fondas F3
47 Pensiones P
48 Pensiones P1
49 Pensiones P2
50 Pensiones P3
51 Pensiones PA
52 Pensiones PT
53 Otros Otras

View File

@@ -0,0 +1,53 @@
"id","ine_code"
base.state_es_c,"ES111"
base.state_es_vi,"ES211"
base.state_es_ab,"ES421"
base.state_es_a,"ES521"
base.state_es_al,"ES611"
base.state_es_o,"ES120"
base.state_es_av,"ES411"
base.state_es_ba,"ES431"
base.state_es_pm,"ES530"
base.state_es_b,"ES511"
base.state_es_bu,"ES412"
base.state_es_cc,"ES432"
base.state_es_ca,"ES612"
base.state_es_s,"ES130"
base.state_es_cs,"ES522"
base.state_es_ce,"ES630"
base.state_es_cr,"ES422"
base.state_es_co,"ES613"
base.state_es_cu,"ES423"
base.state_es_gi,"ES512"
base.state_es_gr,"ES614"
base.state_es_gu,"ES424"
base.state_es_ss,"ES212"
base.state_es_h,"ES615"
base.state_es_hu,"ES241"
base.state_es_j,"ES616"
base.state_es_lo,"ES230"
base.state_es_gc,"ES701"
base.state_es_le,"ES413"
base.state_es_l,"ES513"
base.state_es_lu,"ES112"
base.state_es_m,"ES300"
base.state_es_ma,"ES617"
base.state_es_ml,"ES640"
base.state_es_mu,"ES620"
base.state_es_na,"ES220"
base.state_es_or,"ES113"
base.state_es_p,"ES414"
base.state_es_po,"ES114"
base.state_es_sa,"ES415"
base.state_es_tf,"ES702"
base.state_es_sg,"ES416"
base.state_es_se,"ES618"
base.state_es_so,"ES417"
base.state_es_t,"ES514"
base.state_es_te,"ES242"
base.state_es_to,"ES425"
base.state_es_v,"ES523"
base.state_es_va,"ES418"
base.state_es_bi,"ES213"
base.state_es_za,"ES419"
base.state_es_z,"ES243"
1 id ine_code
2 base.state_es_c ES111
3 base.state_es_vi ES211
4 base.state_es_ab ES421
5 base.state_es_a ES521
6 base.state_es_al ES611
7 base.state_es_o ES120
8 base.state_es_av ES411
9 base.state_es_ba ES431
10 base.state_es_pm ES530
11 base.state_es_b ES511
12 base.state_es_bu ES412
13 base.state_es_cc ES432
14 base.state_es_ca ES612
15 base.state_es_s ES130
16 base.state_es_cs ES522
17 base.state_es_ce ES630
18 base.state_es_cr ES422
19 base.state_es_co ES613
20 base.state_es_cu ES423
21 base.state_es_gi ES512
22 base.state_es_gr ES614
23 base.state_es_gu ES424
24 base.state_es_ss ES212
25 base.state_es_h ES615
26 base.state_es_hu ES241
27 base.state_es_j ES616
28 base.state_es_lo ES230
29 base.state_es_gc ES701
30 base.state_es_le ES413
31 base.state_es_l ES513
32 base.state_es_lu ES112
33 base.state_es_m ES300
34 base.state_es_ma ES617
35 base.state_es_ml ES640
36 base.state_es_mu ES620
37 base.state_es_na ES220
38 base.state_es_or ES113
39 base.state_es_p ES414
40 base.state_es_po ES114
41 base.state_es_sa ES415
42 base.state_es_tf ES702
43 base.state_es_sg ES416
44 base.state_es_se ES618
45 base.state_es_so ES417
46 base.state_es_t ES514
47 base.state_es_te ES242
48 base.state_es_to ES425
49 base.state_es_v ES523
50 base.state_es_va ES418
51 base.state_es_bi ES213
52 base.state_es_za ES419
53 base.state_es_z ES243

View File

@@ -2,3 +2,6 @@
# from . import pms_checkin_partner
from . import pms_property
from . import pms_log_institution_traveller_report
from . import res_country_state
from . import pms_ine_tourism_type_category
from . import pms_room

View File

@@ -0,0 +1,16 @@
from odoo import fields, models
class PmsIneTourismCategory(models.Model):
_name = "pms.ine.tourism.type.category"
_description = "Hotel category in the Ministry of Tourism. Used for INE statistics."
type = fields.Char("Type", required=True)
category = fields.Char("Category", required=True)
def name_get(self):
data = []
for record in self:
display_value = record.category + " (" + record.type + ") "
data.append((record.id, display_value))
return data

View File

@@ -33,6 +33,28 @@ class PmsProperty(models.Model):
string="Institution password",
help="Password provided by institution to send the data.",
)
ine_tourism_number = fields.Char(
"Tourism number",
help="Registration number in the Ministry of Tourism. Used for INE statistics.",
)
ine_seats = fields.Integer(
"Beds available excluding extra beds",
default=0,
help="Used for INE statistics.",
)
ine_permanent_staff = fields.Integer(
"Permanent Staff", default=0, help="Used for INE statistics."
)
ine_eventual_staff = fields.Integer(
"Eventual Staff", default=0, help="Used for INE statistics."
)
ine_unpaid_staff = fields.Integer(
"Unpaid Staff", default=0, help="Used for INE statistics."
)
ine_category_id = fields.Many2one(
"pms.ine.tourism.type.category",
help="Hotel category in the Ministry of Tourism. Used for INE statistics.",
)
def test_connection(self):
headers = {

View File

@@ -0,0 +1,10 @@
from odoo import fields, models
class PmsRoom(models.Model):
_inherit = "pms.room"
in_ine = fields.Boolean(
string="In INE",
help="Take it into account to generate INE statistics",
default=True,
)

View File

@@ -0,0 +1,6 @@
from odoo import fields, models
class ResCountryState(models.Model):
_inherit = "res.country.state"
ine_code = fields.Char(string="INE State Code")

View File

@@ -1,3 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
user_access_traveller_report_wizard,user_access_traveller_report_wizard,model_traveller_report_wizard,pms.group_pms_user,1,1,1,1
user_access_traveller_report_logs,user_access_traveller_report_logs,model_pms_log_institution_traveller_report,pms.group_pms_user,1,1,1,1
user_access_pms_ine_tourism_type_category,user_access_pms_ine_tourism_type_category,model_pms_ine_tourism_type_category,pms.group_pms_user,1,1,1,1
user_access_pms_ine_wizard,user_access_pms_ine_wizard,model_pms_ine_wizard,pms.group_pms_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 user_access_traveller_report_wizard user_access_traveller_report_wizard model_traveller_report_wizard pms.group_pms_user 1 1 1 1
3 user_access_traveller_report_logs user_access_traveller_report_logs model_pms_log_institution_traveller_report pms.group_pms_user 1 1 1 1
4 user_access_pms_ine_tourism_type_category user_access_pms_ine_tourism_type_category model_pms_ine_tourism_type_category pms.group_pms_user 1 1 1 1
5 user_access_pms_ine_wizard user_access_pms_ine_wizard model_pms_ine_wizard pms.group_pms_user 1 1 1 1

View File

@@ -0,0 +1 @@
from . import test_wizard_ine

View File

@@ -0,0 +1,29 @@
from odoo.tests import common
class TestPms(common.SavepointCase):
def setUp(self):
super().setUp()
self.pricelist1 = self.env["product.pricelist"].create(
{
"name": "Pricelist 1",
}
)
self.company1 = self.env["res.company"].create(
{
"name": "Company 1",
}
)
self.pms_property1 = self.env["pms.property"].create(
{
"name": "Property 1",
"company_id": self.company1.id,
"default_pricelist_id": self.pricelist1.id,
}
)
self.room_type_class1 = self.env["pms.room.type.class"].create(
{
"name": "Room Type Class 1",
"default_code": "RTC1",
}
)

View File

@@ -0,0 +1,632 @@
import datetime
from freezegun import freeze_time
from .common import TestPms
@freeze_time("2021-02-01")
class TestWizardINE(TestPms):
def setUp(self):
super().setUp()
# number of seats established in the property
self.pms_property1.ine_seats = 50
# create room types
self.room_type = self.env["pms.room.type"].create(
{
"name": "Room type test",
"default_code": "DBL_Test",
"class_id": self.room_type_class1.id,
}
)
# create rooms
self.room_double_1 = self.env["pms.room"].create(
{
"pms_property_id": self.pms_property1.id,
"name": "Room test 1",
"room_type_id": self.room_type.id,
"capacity": 2,
}
)
self.room_double_2 = self.env["pms.room"].create(
{
"pms_property_id": self.pms_property1.id,
"name": "Room test 2",
"room_type_id": self.room_type.id,
"capacity": 2,
}
)
self.room_single_1 = self.env["pms.room"].create(
{
"pms_property_id": self.pms_property1.id,
"name": "Room test 3",
"room_type_id": self.room_type.id,
"capacity": 1,
"extra_beds_allowed": 1,
}
)
self.room_triple1 = self.env["pms.room"].create(
{
"pms_property_id": self.pms_property1.id,
"name": "Room test 4",
"room_type_id": self.room_type.id,
"capacity": 3,
}
)
# create document category
self.id_category_passport = self.env["res.partner.id_category"].create(
{
"name": "Passport",
"code": "P",
"active": True,
}
)
# get records of russia, italy and afghanistan
self.country_russia = self.env["res.country"].search([("code", "=", "RU")])
self.country_russia.ensure_one()
self.country_italy = self.env["res.country"].search([("code", "=", "IT")])
self.country_italy.ensure_one()
self.country_afghanistan = self.env["res.country"].search([("code", "=", "AF")])
self.country_afghanistan.ensure_one()
# Create partner 1 (italy)
self.partner_1 = self.env["res.partner"].create(
{
"name": "partner1",
"country_id": self.country_italy.id,
"nationality_id": self.country_italy.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
)
self.env["res.partner.id_number"].create(
{
"category_id": self.id_category_passport.id,
"name": "55103354T",
"valid_from": datetime.date.today(),
"partner_id": self.partner_1.id,
}
)
# Create partner 2 (russia)
self.partner_2 = self.env["res.partner"].create(
{
"name": "partner2",
"country_id": self.country_russia.id,
"nationality_id": self.country_russia.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
)
self.env["res.partner.id_number"].create(
{
"category_id": self.id_category_passport.id,
"name": "45437298Q",
"valid_from": datetime.date.today(),
"partner_id": self.partner_2.id,
}
)
# Create partner 3 (italy)
self.partner_3 = self.env["res.partner"].create(
{
"name": "partner3",
"country_id": self.country_italy.id,
"nationality_id": self.country_italy.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
)
self.env["res.partner.id_number"].create(
{
"category_id": self.id_category_passport.id,
"name": "81534086Y",
"valid_from": datetime.date.today(),
"partner_id": self.partner_3.id,
}
)
# Create partner 4 (italy)
self.partner_4 = self.env["res.partner"].create(
{
"name": "partner4",
"country_id": self.country_italy.id,
"nationality_id": self.country_italy.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
)
self.env["res.partner.id_number"].create(
{
"category_id": self.id_category_passport.id,
"name": "00807643K",
"valid_from": datetime.date.today(),
"partner_id": self.partner_4.id,
}
)
# Create partner 5 (afghanistan)
self.partner_5 = self.env["res.partner"].create(
{
"name": "partner5",
"country_id": self.country_afghanistan.id,
"nationality_id": self.country_afghanistan.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
)
self.env["res.partner.id_number"].create(
{
"category_id": self.id_category_passport.id,
"name": "54564399G",
"valid_from": datetime.date.today(),
"partner_id": self.partner_5.id,
}
)
# Create partner 6 (afghanistan)
self.partner_6 = self.env["res.partner"].create(
{
"name": "partner6",
"country_id": self.country_afghanistan.id,
"nationality_id": self.country_afghanistan.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
)
self.env["res.partner.id_number"].create(
{
"category_id": self.id_category_passport.id,
"name": "39854152M",
"valid_from": datetime.date.today(),
"partner_id": self.partner_6.id,
}
)
# Create partner 7 (afghanistan)
self.partner_7 = self.env["res.partner"].create(
{
"name": "partner7",
"country_id": self.country_afghanistan.id,
"nationality_id": self.country_afghanistan.id,
"birthdate_date": "2000-06-25",
"gender": "male",
}
)
self.env["res.partner.id_number"].create(
{
"category_id": self.id_category_passport.id,
"name": "39854152O",
"valid_from": datetime.date.today(),
"partner_id": self.partner_7.id,
}
)
# Create reservation 1
self.reservation_1 = self.env["pms.reservation"].create(
{
"checkin": datetime.date.today(),
"checkout": datetime.date.today() + datetime.timedelta(days=1),
"preferred_room_id": self.room_double_1.id,
"partner_id": self.partner_1.id,
"adults": 2,
"pms_property_id": self.pms_property1.id,
}
)
self.checkin1 = self.env["pms.checkin.partner"].create(
{
"partner_id": self.partner_1.id,
"reservation_id": self.reservation_1.id,
}
)
self.checkin2 = self.env["pms.checkin.partner"].create(
{
"partner_id": self.partner_2.id,
"reservation_id": self.reservation_1.id,
}
)
# Create reservation 2
self.reservation_2 = self.env["pms.reservation"].create(
{
"checkin": datetime.date.today() + datetime.timedelta(days=1),
"checkout": datetime.date.today() + datetime.timedelta(days=2),
"preferred_room_id": self.room_triple1.id,
"partner_id": self.partner_3.id,
"adults": 2,
"pms_property_id": self.pms_property1.id,
}
)
self.checkin3 = self.env["pms.checkin.partner"].create(
{
"partner_id": self.partner_3.id,
"reservation_id": self.reservation_2.id,
}
)
self.checkin4 = self.env["pms.checkin.partner"].create(
{
"partner_id": self.partner_4.id,
"reservation_id": self.reservation_2.id,
}
)
# Create reservation 3
self.reservation_3 = self.env["pms.reservation"].create(
{
"checkin": datetime.date.today() + datetime.timedelta(days=1),
"checkout": datetime.date.today() + datetime.timedelta(days=3),
"preferred_room_id": self.room_double_2.id,
"partner_id": self.partner_5.id,
"adults": 1,
"pms_property_id": self.pms_property1.id,
}
)
self.checkin5 = self.env["pms.checkin.partner"].create(
{
"partner_id": self.partner_5.id,
"reservation_id": self.reservation_3.id,
}
)
# Create extra bed service
product_extra_bed = self.env["product.product"].create(
{
"name": "Product test",
"is_extra_bed": True,
"consumed_on": "before",
"per_day": True,
}
)
vals_service_extra_bed = {
"is_board_service": False,
"product_id": product_extra_bed.id,
}
# Create reservation 4
self.reservation_4 = self.env["pms.reservation"].create(
{
"checkin": datetime.date.today() + datetime.timedelta(days=1),
"checkout": datetime.date.today() + datetime.timedelta(days=3),
"preferred_room_id": self.room_single_1.id,
"partner_id": self.partner_6.id,
"adults": 2,
"pms_property_id": self.pms_property1.id,
"service_ids": [(0, 0, vals_service_extra_bed)],
}
)
self.checkin6 = self.env["pms.checkin.partner"].create(
{
"partner_id": self.partner_6.id,
"reservation_id": self.reservation_4.id,
}
)
self.checkin7 = self.env["pms.checkin.partner"].create(
{
"partner_id": self.partner_7.id,
"reservation_id": self.reservation_4.id,
}
)
# checkin partners on board
self.checkin1.action_on_board()
self.checkin2.action_on_board()
with freeze_time("2021-02-02"):
self.checkin3.action_on_board()
self.checkin4.action_on_board()
self.checkin5.action_on_board()
self.checkin6.action_on_board()
self.checkin7.action_on_board()
# set prices for nights
self.reservation_1.reservation_line_ids[0].price = 25.0
self.reservation_2.reservation_line_ids[0].price = 21.0
self.reservation_3.reservation_line_ids[0].price = 25.0
self.reservation_3.reservation_line_ids[1].price = 25.0
self.reservation_4.reservation_line_ids[0].price = 21.50
self.reservation_4.reservation_line_ids[1].price = 21.50
def test_room_type_num_by_date(self):
"""
+============================+==============+==============+=============+
| | 01 | 02 | 03 |
+============================+==============+==============+=============+
| r1 2 adults | DOUBLE ROOM | | |
| r2 2 adults | | TRIPLE ROOM | |
| r3 1 adult | | DOUBLE ROOM | DOUBLE ROOM |
| r4 2 adults (1 extra bed) | | SINGLE ROOM | SINGLE ROOM |
+============================+==============+==============+=============+
| double rooms (use double) | 1 | 0 | 0 |
+============================+==============+==============+=============+
| double rooms (use single) | 0 | 1 | 1 |
+============================+==============+==============+=============+
| other rooms | 0 | 2 | 1 |
+============================+==============+==============+=============+
| extra beds | 0 | 1 | 1 |
+============================+==============+==============+=============+
"""
# ARRANGE
start_date = datetime.date(2021, 2, 1)
second_date = datetime.date(2021, 2, 2)
end_date = datetime.date(2021, 2, 3)
expected_result = {
start_date: {
"double_rooms_double_use": 1,
"double_rooms_single_use": 0,
"other_rooms": 0,
"extra_beds": 0,
},
second_date: {
"double_rooms_double_use": 0,
"double_rooms_single_use": 1,
"other_rooms": 2,
"extra_beds": 1,
},
end_date: {
"double_rooms_double_use": 0,
"double_rooms_single_use": 1,
"other_rooms": 1,
"extra_beds": 1,
},
}
# ACT
rooms = self.env["pms.ine.wizard"].ine_rooms(
start_date, end_date, self.pms_property1
)
# ASSERT
self.assertDictEqual(rooms, expected_result)
def test_arrivals_departures_pernoctations_by_date(self):
"""
+===========================+==============+==============+=============+=============+
| | 01 | 02 | 03 | 04 |
+===========================+==============+==============+=============+=============+
| r1 2 adults | italy,russia | italy,russia | | |
+---------------------------+--------------+--------------+-------------+-------------+
| r2 2 adults | | italy,italy | italy,italy | |
+---------------------------+--------------+--------------+-------------+-------------+
| r3 1 adult | | afghanistan | afghanistan | afghanistan |
+---------------------------+--------------+--------------+-------------+-------------+
| r4 2 adults | | afghanistan | afghanistan | afghanistan |
| | | afghanistan | afghanistan | afghanistan |
+===========================+==============+==============+=============+=============+
| arrivals Afghanistan | | 3 | | |
| arrivals Italy | 1 | 2 | | |
| arrivals Russia | 1 | | | |
+===========================+==============+==============+=============+=============+
| pernoctations Afghanistan | | 3 | 3 | |
| pernoctations Italy | 1 | 2 | | |
| pernoctations Russia | 1 | | | |
+===========================+==============+==============+=============+=============+
| departures Afghanistan | | | | 3 |
| departures Italy | | 1 | 2 | |
| departures Russia | | 1 | | |
+===========================+==============+==============+=============+=============+
"""
# ARRANGE
start_date = datetime.date(2021, 2, 1)
second_date = datetime.date(2021, 2, 2)
third_date = datetime.date(2021, 2, 3)
end_date = datetime.date(2021, 2, 4)
expected_result = {
self.country_afghanistan.code: {
second_date: {
"arrivals": 3,
"pernoctations": 3,
},
third_date: {
"pernoctations": 3,
},
end_date: {
"departures": 3,
},
},
self.country_italy.code: {
start_date: {
"arrivals": 1,
"pernoctations": 1,
},
second_date: {
"arrivals": 2,
"pernoctations": 2,
"departures": 1,
},
third_date: {
"departures": 2,
},
},
self.country_russia.code: {
start_date: {
"arrivals": 1,
"pernoctations": 1,
},
second_date: {
"departures": 1,
},
},
}
# ACT
nationalities = self.env["pms.ine.wizard"].ine_nationalities(
start_date, end_date, self.pms_property1.id
)
# ASSERT
self.assertDictEqual(nationalities, expected_result)
def test_spain_arrivals_departures_pernoctations_by_date(self):
"""
+==========================+============+============+=========+========+
| | 01 | 02 | 03 | 04 |
+==========================+============+============+=========+========+
| r1 2 adults | Ourense | Ourense | | |
| | Pontevedra | Pontevedra | | |
+--------------------------+------------+------------+---------+--------+
| r2 2 adults | | Ourense | Ourense | |
| | | Ourense | Ourense | |
+--------------------------+------------+------------+---------+--------+
| r3 1 adult | | Madrid | Madrid | Madrid |
+--------------------------+------------+------------+---------+--------+
| r4 2 adults | | Madrid | Madrid | Madrid |
| | | Madrid | Madrid | Madrid |
+==========================+============+============+=========+========+
| arrivals Madrid | | 3 | | |
| arrivals Ourense | 1 | 2 | | |
| arrivals Pontevedra | 1 | | | |
+==========================+============+============+=========+========+
| pernoctations Madrid | | 3 | 3 | |
| pernoctations Ourense | 1 | 2 | | |
| pernoctations Pontevedra | 1 | | | |
0+=========================+============+============+=========+========+
| departures Madrid | | | | 3 |
| departures Ourense | | 1 | 2 | |
| departures Pontevedra | | 1 | | |
+==========================+============+============+=========+========+
"""
# ARRANGE
start_date = datetime.date(2021, 2, 1)
second_date = datetime.date(2021, 2, 2)
third_date = datetime.date(2021, 2, 3)
end_date = datetime.date(2021, 2, 4)
country_spain = self.env["res.country"].search([("code", "=", "ES")])
state_madrid = self.env["res.country.state"].search([("name", "=", "Madrid")])
state_ourense = self.env["res.country.state"].search(
[("name", "=", "Ourense (Orense)")]
)
state_pontevedra = self.env["res.country.state"].search(
[("name", "=", "Pontevedra")]
)
self.checkin1.nationality_id = country_spain
self.partner_1.nationality_id = country_spain
self.checkin1.state_id = state_ourense
self.partner_1.state_id = state_ourense
self.checkin2.nationality_id = country_spain
self.partner_2.nationality_id = country_spain
self.checkin2.state_id = state_pontevedra
self.partner_2.state_id = state_pontevedra
self.checkin3.nationality_id = country_spain
self.partner_3.nationality_id = country_spain
self.checkin3.state_id = state_ourense
self.partner_3.state_id = state_ourense
self.checkin4.nationality_id = country_spain
self.partner_4.nationality_id = country_spain
self.checkin4.state_id = state_ourense
self.partner_4.state_id = state_ourense
self.checkin5.nationality_id = country_spain
self.partner_5.nationality_id = country_spain
self.checkin5.state_id = state_madrid
self.partner_5.state_id = state_madrid
self.checkin6.nationality_id = country_spain
self.partner_6.nationality_id = country_spain
self.checkin6.state_id = state_madrid
self.partner_6.state_id = state_madrid
self.checkin7.nationality_id = country_spain
self.partner_7.nationality_id = country_spain
self.checkin7.state_id = state_madrid
self.partner_7.state_id = state_madrid
expected_result = {
country_spain.code: {
state_madrid.ine_code: {
second_date: {
"arrivals": 3,
"pernoctations": 3,
},
third_date: {
"pernoctations": 3,
},
end_date: {
"departures": 3,
},
},
state_ourense.ine_code: {
start_date: {
"arrivals": 1,
"pernoctations": 1,
},
second_date: {
"arrivals": 2,
"pernoctations": 2,
"departures": 1,
},
third_date: {
"departures": 2,
},
},
state_pontevedra.ine_code: {
start_date: {
"arrivals": 1,
"pernoctations": 1,
},
second_date: {
"departures": 1,
},
},
}
}
# ACT
nationalities = self.env["pms.ine.wizard"].ine_nationalities(
start_date, end_date, self.pms_property1.id
)
# ASSERT
self.assertDictEqual(nationalities, expected_result)
def test_calculate_monthly_adr(self):
"""
+-------------+-------+-------+-------+
| | 01 | 02 | 03 |
+-------------+-------+-------+-------+
| r1 | 25.00 | | |
| r2 | | 21.00 | |
| r3 | | 25.00 | 25.00 |
| r4 | | 21.50 | 21.50 |
+-------------+-------+-------+-------+
| adr | 25.00 | 22.50 | 23.25 |
+-------------+-------+-------+-------+
| monthly adr | 23.58 |
+-------------+-------+-------+-------+
"""
# ARRANGE
start_date = datetime.date(2021, 2, 1)
expected_monthly_adr = 23.58
# ACT
monthly_adr = self.env["pms.ine.wizard"].ine_calculate_monthly_adr(
start_date, self.pms_property1.id
)
# ASSERT
self.assertEqual(
expected_monthly_adr,
monthly_adr,
)
def test_calculate_monthly_revpar(self):
"""
+----------------+-------+-------+-------+
| | 01 | 02 | 03 |
+----------------+-------+-------+-------+
| r1 | 25.00 | | |
| r2 | | 21.00 | |
| r3 | | 25.00 | 25.00 |
| r4 | | 21.50 | 21.50 |
+----------------+-------+-------+-------+
| monthly revpar | 23.58 |
+----------------+-------+-------+-------+
num rooms avail. = 4
income = 25.00 + 21.00 + 25.00 + 25.00 + 21.50 + 21.50 = 139
monthly revpar = 139 / (4 * 28)
"""
# ARRANGE
start_date = datetime.date(2021, 2, 1)
expected_monthly_revpar = 1.24
# ACT
monthly_revpar = self.env["pms.ine.wizard"].ine_calculate_monthly_revpar(
start_date, self.pms_property1.id
)
# ASSERT
self.assertEqual(
expected_monthly_revpar,
monthly_revpar,
)

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="pms_ine_tourism_category_view_form">
<field name="name">pms.ine.tourism.category.form</field>
<field name="model">pms.ine.tourism.type.category</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="type" />
<field name="category" />
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="pms_ine_tourism_category_view_tree">
<field name="name">pms.ine.tourism.category.tree</field>
<field name="model">pms.ine.tourism.type.category</field>
<field name="arch" type="xml">
<tree>
<field name="type" />
<field name="category" />
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="open_pms_ine_tourism_category">
<field name="name">INE Categories</field>
<field name="res_model">pms.ine.tourism.type.category</field>
<field name="view_mode">tree,form</field>
</record>
<!-- <menuitem-->
<!-- name="INE Tourism Categories"-->
<!-- id="menu_open_pms_ine_tourism_category_form_tree"-->
<!-- action="open_pms_ine_tourism_category"-->
<!-- parent="pms.menu_reservations"-->
<!-- sequence="30"-->
<!-- />-->
</odoo>

View File

@@ -34,6 +34,27 @@
/>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="o_horizontal_separator">
INE Settings
</div>
</div>
<div class="col-6">
<group>
<field name="ine_tourism_number" />
<field name="ine_category_id" />
</group>
</div>
<div class="col-6">
<group>
<field name="ine_seats" />
<field name="ine_permanent_staff" />
<field name="ine_eventual_staff" />
<field name="ine_unpaid_staff" />
</group>
</div>
</div>
</xpath>
</field>
</record>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_room_form_pms_l10n_es" model="ir.ui.view">
<field name="name">Property Form l10n_es</field>
<field name="model">pms.room</field>
<field name="inherit_id" ref="pms.pms_room_view_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='sequence']" position="after">
<field name="in_ine" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -1 +1,2 @@
from . import traveller_report
from . import wizard_ine

View File

@@ -0,0 +1,680 @@
import base64
import calendar
import datetime
import xml.etree.cElementTree as ET
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
# TODO: Review code (code iso ?)
CODE_SPAIN = "ES"
class WizardIne(models.TransientModel):
_name = "pms.ine.wizard"
_description = "Wizard to generate statistical info."
pms_property_id = fields.Many2one(
string="Property",
comodel_name="pms.property",
default=lambda self: self.env["pms.property"].browse(
self.env.user.get_active_property_ids()[0]
),
check_pms_properties=True,
required=True,
)
txt_filename = fields.Text()
txt_binary = fields.Binary(string="File Download")
txt_message = fields.Char(string="File Preview")
start_date = fields.Date(
string="From",
required=True,
)
end_date = fields.Date(
string="To",
required=True,
)
adr = fields.Float(string="Monthly ADR")
revpar = fields.Float(string="Monthly RevPAR")
@api.model
def ine_rooms(self, start_date, end_date, pms_property_id):
"""
Returns a dictionary:
{
date_1: {
'double_rooms_single_use': number,
'double_rooms_double_use': number,
'other_rooms': number,
'extra_beds': number
},
# ... more dates
}
"""
# result object
rooms = dict()
# iterate days between start_date and end_date
for p_date in [
start_date + datetime.timedelta(days=x)
for x in range(0, (end_date - start_date).days + 1)
]:
# rooms with capacity 2 but only 1 adult using them
double_rooms_single_use = (
self.env["pms.reservation.line"]
.search(
[
("pms_property_id", "=", pms_property_id.id),
("occupies_availability", "=", True),
("reservation_id.reservation_type", "=", "normal"),
("room_id.in_ine", "=", True),
("date", "=", p_date),
("room_id.capacity", "=", 2),
("reservation_id.adults", "=", 1),
]
)
.mapped("room_id")
)
# rooms with capacity 2 with 2 adult using them
double_rooms_double_use = (
self.env["pms.reservation.line"]
.search(
[
("pms_property_id", "=", pms_property_id.id),
("occupies_availability", "=", True),
("reservation_id.reservation_type", "=", "normal"),
("room_id.in_ine", "=", True),
("date", "=", p_date),
("room_id.capacity", "=", 2),
("reservation_id.adults", "=", 2),
]
)
.mapped("room_id")
)
# service lines with extra beds
extra_bed_service_lines = self.env["pms.service.line"].search(
[
("pms_property_id", "=", pms_property_id.id),
("product_id.is_extra_bed", "=", True),
("reservation_id.reservation_type", "=", "normal"),
("date", "=", p_date),
]
)
extra_beds = 0
# get num. extra beds
for ebsl in extra_bed_service_lines:
reservation_lines = ebsl.reservation_id.reservation_line_ids.filtered(
lambda x: x.date == ebsl.date
and x.room_id.in_ine
and x.occupies_availability
)
if reservation_lines:
extra_beds += (
ebsl.day_qty
- reservation_lines.reservation_id.children_occupying
)
# children occuppying do not have checkin partner data
# search all rooms
all_rooms = (
self.env["pms.reservation.line"]
.search(
[
("date", "=", p_date),
("occupies_availability", "=", True),
("reservation_id.reservation_type", "=", "normal"),
("room_id.in_ine", "=", True),
("pms_property_id", "=", pms_property_id.id),
]
)
.mapped("room_id")
)
# other rooms = all rooms - double rooms
other_rooms = (
all_rooms - double_rooms_double_use
) - double_rooms_single_use
# no room movements -> no dict entrys
if not (
extra_beds == 0
and len(other_rooms) == 0
and len(double_rooms_double_use) == 0
and len(double_rooms_single_use) == 0
):
# create result dict for each date
rooms[p_date] = dict()
rooms[p_date]["double_rooms_single_use"] = len(double_rooms_single_use)
rooms[p_date]["double_rooms_double_use"] = len(double_rooms_double_use)
rooms[p_date]["other_rooms"] = len(other_rooms)
rooms[p_date]["extra_beds"] = extra_beds
return rooms
@api.model
def ine_nationalities(self, start_date, end_date, pms_property_id):
"""
Returns a dictionary:
{
CODE_SPAIN: {
state.code_ine: {
date: {
'arrivals': number,
'departures': number,
'pernoctations': number,
},
# ... more dates
},
# ... more ine codes from spain
},
# ... more countries (except Spain)
country.code_alpha3: {
date: {
'arrivals': num. of arrivals
'departures': num. of departures
'pernoctations': num. of pernoctations
},
# ... more dates
},
# ... more countries (except Spain)
}
"""
def ine_add_arrivals_departures_pernoctations(
date, type_of_entry, read_group_result
):
"""
date = date to add the entry to dic
type_of_entry = 'arrivals' | 'departures' | 'pernoctations'
read_group_result = result of read_group by type_of_entry
"""
for entry in read_group_result:
# get nationality_id from group set read_group results
nationality_id_code = (
self.env["res.country"]
.search([("id", "=", entry["nationality_id"][0])])
.code
)
# all countries except Spain
if nationality_id_code != CODE_SPAIN:
# get count of each result
num = entry["__count"]
# update/create dicts for countries & dates and set num. arrivals
if not nationalities.get(nationality_id_code):
nationalities[nationality_id_code] = dict()
if not nationalities[nationality_id_code].get(date):
nationalities[nationality_id_code][date] = dict()
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(
entry["__domain"],
["state_id"],
["state_id"],
lazy=False,
)
# iterate read_group results from Spain
for entry_from_spain in read_by_arrivals_spain:
state_id = self.env["res.country.state"].browse(
entry_from_spain["state_id"][0]
) # .ine_code
ine_code = state_id.ine_code
# get count of each result
num_spain = entry_from_spain["__count"]
# update/create dicts for states & dates and set num. arrivals
if not nationalities.get(CODE_SPAIN):
nationalities[CODE_SPAIN] = dict()
if not nationalities[CODE_SPAIN].get(ine_code):
nationalities[CODE_SPAIN][ine_code] = dict()
if not nationalities[CODE_SPAIN][ine_code].get(date):
nationalities[CODE_SPAIN][ine_code][date] = dict()
nationalities[CODE_SPAIN][ine_code][date][
type_of_entry
] = num_spain
# result object
nationalities = dict()
# iterate days between start_date and end_date
for p_date in [
start_date + datetime.timedelta(days=x)
for x in range(0, (end_date - start_date).days + 1)
]:
# search for checkin partners
hosts = self.env["pms.checkin.partner"].search(
[
("partner_id", "!=", False),
("pms_property_id", "=", pms_property_id),
("checkin", "<=", p_date),
("checkout", ">=", p_date),
]
)
# only checkin partners housed in "in_ine" rooms
hosts = hosts.filtered(
lambda x: x.reservation_id.reservation_line_ids.mapped("room_id").in_ine
)
# arrivals
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)],
["nationality_id"],
["nationality_id"],
lazy=False,
)
# departures
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)],
["nationality_id"],
["nationality_id"],
lazy=False,
)
# pernoctations
pernoctations = hosts - departures
# pernoctations grouped by nationality_id
read_by_pernoctations = self.env["res.partner"].read_group(
[("id", "in", pernoctations.mapped("partner_id").ids)],
["nationality_id"],
["nationality_id"],
lazy=False,
)
ine_add_arrivals_departures_pernoctations(
p_date, "arrivals", read_by_arrivals
)
ine_add_arrivals_departures_pernoctations(
p_date, "departures", read_by_departures
)
ine_add_arrivals_departures_pernoctations(
p_date, "pernoctations", read_by_pernoctations
)
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)
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"],
[],
)
rooms_not_allowed = (
self.env["pms.reservation.line"]
.search(
[
("pms_property_id", "=", pms_property_id),
("occupies_availability", "=", True),
("reservation_id.reservation_type", "!=", "normal"),
]
)
.mapped("room_id")
.ids
)
available_rooms = self.env["pms.room"].search_count(
[
("in_ine", "=", True),
("pms_property_id", "=", pms_property_id),
("id", "not in", rooms_not_allowed),
]
)
if not sum_group_price[0]["price"]:
return 0
revpar = round(
sum_group_price[0]["price"] / (available_rooms * last_day.day), 2
)
self.revpar = revpar
return revpar
@api.model
def ine_get_nif_cif(self, cif_nif):
country_codes = self.env["res.country"].search([]).mapped("code")
if cif_nif[:2] in country_codes:
return cif_nif[2:].strip()
return cif_nif.strip()
@api.model
def check_ine_mandatory_fields(self, pms_property_id):
if not pms_property_id.name:
raise ValidationError(_("The property name is not established."))
if not pms_property_id.company_id.vat:
raise ValidationError(_("The company VAT is not established."))
if not pms_property_id.company_id.name:
raise ValidationError(_("The company name is not established."))
if not pms_property_id.name:
raise ValidationError(_("The property name is not established."))
if not pms_property_id.ine_tourism_number:
raise ValidationError(_("The property tourism number is not established."))
if not pms_property_id.ine_tourism_number:
raise ValidationError(_("The property tourism number is not established."))
if not pms_property_id.street:
raise ValidationError(_("The property street is not established."))
if not pms_property_id.zip:
raise ValidationError(_("The property zip is not established."))
if not pms_property_id.city:
raise ValidationError(_("The property city is not established."))
if not pms_property_id.partner_id.state_id:
raise ValidationError(_("The property state is not established."))
if not pms_property_id.phone:
raise ValidationError(_("The property phone is not established."))
if not pms_property_id.ine_category_id:
raise ValidationError(_("The property category is not established."))
def ine_generate_xml(self):
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(
[
("in_ine", "=", True),
("pms_property_id", "=", self.pms_property_id.id),
]
)
.mapped("capacity")
)
if number_of_rooms > self.pms_property_id.ine_seats:
raise ValidationError(
_(
"The number of seats, excluding extra beds (%s)"
% str(number_of_rooms)
+ " exceeds the number of seats established in the property (%s)"
% str(self.pms_property_id.ine_seats)
)
)
# INE XML
survey_tag = ET.Element("ENCUESTA")
# INE XML -> PROPERTY
header_tag = ET.SubElement(survey_tag, "CABECERA")
date = ET.SubElement(header_tag, "FECHA_REFERENCIA")
ET.SubElement(date, "MES").text = f"{self.start_date.month:02}"
ET.SubElement(date, "ANYO").text = str(self.start_date.year)
ET.SubElement(header_tag, "DIAS_ABIERTO_MES_REFERENCIA").text = str(
calendar.monthrange(self.start_date.year, self.start_date.month)[1]
)
ET.SubElement(
header_tag, "RAZON_SOCIAL"
).text = self.pms_property_id.company_id.name
ET.SubElement(
header_tag, "NOMBRE_ESTABLECIMIENTO"
).text = self.pms_property_id.name
ET.SubElement(header_tag, "CIF_NIF").text = self.ine_get_nif_cif(
self.pms_property_id.company_id.vat
)
ET.SubElement(
header_tag, "NUMERO_REGISTRO"
).text = self.pms_property_id.ine_tourism_number
ET.SubElement(header_tag, "DIRECCION").text = self.pms_property_id.street
ET.SubElement(header_tag, "CODIGO_POSTAL").text = self.pms_property_id.zip
ET.SubElement(header_tag, "LOCALIDAD").text = self.pms_property_id.city
ET.SubElement(header_tag, "MUNICIPIO").text = self.pms_property_id.city
ET.SubElement(
header_tag, "PROVINCIA"
).text = self.pms_property_id.partner_id.state_id.name
ET.SubElement(
header_tag, "TELEFONO_1"
).text = self.pms_property_id.phone.replace(" ", "")[0:12]
ET.SubElement(
header_tag, "TIPO"
).text = self.pms_property_id.ine_category_id.type
ET.SubElement(
header_tag, "CATEGORIA"
).text = self.pms_property_id.ine_category_id.category
ET.SubElement(header_tag, "HABITACIONES").text = str(
self.env["pms.room"].search_count([("in_ine", "=", True)])
)
ET.SubElement(header_tag, "PLAZAS_DISPONIBLES_SIN_SUPLETORIAS").text = str(
self.pms_property_id.ine_seats
)
ET.SubElement(header_tag, "URL").text = self.pms_property_id.website
# INE XML -> GUESTS
accommodation_tag = ET.SubElement(survey_tag, "ALOJAMIENTO")
nationalities = self.ine_nationalities(
self.start_date, self.end_date, self.pms_property_id.id
)
for key_country, value_country in nationalities.items():
country = self.env["res.country"].search([("code", "=", key_country)])
if key_country != CODE_SPAIN:
residency_tag = ET.SubElement(accommodation_tag, "RESIDENCIA")
ET.SubElement(residency_tag, "ID_PAIS").text = country.code_alpha3
for key_date, value_dates in value_country.items():
movement = ET.SubElement(residency_tag, "MOVIMIENTO")
ET.SubElement(movement, "N_DIA").text = f"{key_date.day:02}"
num_arrivals = (
value_dates["arrivals"] if value_dates.get("arrivals") else 0
)
num_departures = (
value_dates["departures"]
if value_dates.get("departures")
else 0
)
num_pernoctations = (
value_dates["pernoctations"]
if value_dates.get("pernoctations")
else 0
)
ET.SubElement(movement, "ENTRADAS").text = str(num_arrivals)
ET.SubElement(movement, "SALIDAS").text = str(num_departures)
ET.SubElement(movement, "PERNOCTACIONES").text = str(
num_pernoctations
)
else:
for code_ine, value_state in value_country.items():
residency_tag = ET.SubElement(accommodation_tag, "RESIDENCIA")
ET.SubElement(residency_tag, "ID_PROVINCIA_ISLA").text = code_ine
for key_date, value_dates in value_state.items():
movement = ET.SubElement(residency_tag, "MOVIMIENTO")
ET.SubElement(movement, "N_DIA").text = f"{key_date.day:02}"
num_arrivals = (
value_dates["arrivals"]
if value_dates.get("arrivals")
else 0
)
num_departures = (
value_dates["departures"]
if value_dates.get("departures")
else 0
)
num_pernoctations = (
value_dates["pernoctations"]
if value_dates.get("pernoctations")
else 0
)
ET.SubElement(movement, "ENTRADAS").text = str(num_arrivals)
ET.SubElement(movement, "SALIDAS").text = str(num_departures)
ET.SubElement(movement, "PERNOCTACIONES").text = str(
num_pernoctations
)
rooms_tag = ET.SubElement(survey_tag, "HABITACIONES")
rooms = self.ine_rooms(self.start_date, self.end_date, self.pms_property_id)
# INE XML -> ROOMS
for key_date, value_rooms in rooms.items():
rooms_move = ET.SubElement(rooms_tag, "HABITACIONES_MOVIMIENTO")
ET.SubElement(rooms_move, "HABITACIONES_N_DIA").text = f"{key_date.day:02}"
ET.SubElement(rooms_move, "PLAZAS_SUPLETORIAS").text = str(
value_rooms["extra_beds"]
)
ET.SubElement(rooms_move, "HABITACIONES_DOBLES_USO_DOBLE").text = str(
value_rooms["double_rooms_double_use"]
)
ET.SubElement(rooms_move, "HABITACIONES_DOBLES_USO_INDIVIDUAL").text = str(
value_rooms["double_rooms_single_use"]
)
ET.SubElement(rooms_move, "HABITACIONES_OTRAS").text = str(
value_rooms["other_rooms"]
)
prices_tag = ET.SubElement(survey_tag, "PRECIOS")
ET.SubElement(prices_tag, "REVPAR_MENSUAL").text = str(
self.ine_calculate_monthly_revpar(
self.start_date,
self.pms_property_id.id,
)
)
ET.SubElement(prices_tag, "ADR_MENSUAL").text = str(
self.ine_calculate_monthly_adr(
self.start_date,
self.pms_property_id.id,
)
)
# TODO:
# Evaluate how to get occupation & ADR for:
# -traditional/online tour-operator
# -traditional/online agency
# -companys
ET.SubElement(prices_tag, "ADR_TOUROPERADOR_TRADICIONAL").text = "0"
ET.SubElement(
prices_tag, "PCTN_HABITACIONES_OCUPADAS_TOUROPERADOR_TRADICIONAL"
).text = "0"
ET.SubElement(prices_tag, "ADR_TOUROPERADOR_ONLINE").text = "0"
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, "PCTN_HABITACIONES_OCUPADAS_AGENCIA_TRADICIONAL"
).text = "0"
ET.SubElement(prices_tag, "ADR_AGENCIA_DE_VIAJE_ONLINE").text = "0"
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"
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"
staff_tag = ET.SubElement(survey_tag, "PERSONAL_OCUPADO")
ET.SubElement(staff_tag, "PERSONAL_NO_REMUNERADO").text = str(
self.pms_property_id.ine_unpaid_staff
)
ET.SubElement(staff_tag, "PERSONAL_REMUNERADO_FIJO").text = str(
self.pms_property_id.ine_permanent_staff
)
ET.SubElement(staff_tag, "PERSONAL_REMUNERADO_EVENTUAL").text = str(
self.pms_property_id.ine_eventual_staff
)
xmlstr = '<?xml version="1.0" encoding="ISO-8859-1"?>'
xmlstr += ET.tostring(survey_tag).decode("utf-8")
self.txt_binary = base64.b64encode(str.encode(xmlstr))
self.txt_filename = (
"INE_"
+ str(self.start_date.month)
+ "_"
+ str(self.start_date.year)
+ ".xml"
)
return {
"context": self.env.context,
"view_type": "form",
"view_mode": "form",
"res_model": "pms.ine.wizard",
"res_id": self.id,
"view_id": False,
"type": "ir.actions.act_window",
"target": "new",
}

View File

@@ -0,0 +1,102 @@
<?xml version="1.0" ?>
<odoo>
<record id="pms_ine_wizard" model="ir.ui.view">
<field name="name">INE</field>
<field name="model">pms.ine.wizard</field>
<field name="arch" type="xml">
<form>
<div class="row">
<div class="col-6">
<group>
<field name="pms_property_id" />
</group>
</div>
<div class="col-6">
<group>
<field
name="start_date"
widget="daterange"
options="{'related_end_date': 'end_date'}"
/>
<field
name="end_date"
widget="daterange"
options="{'related_start_date': 'start_date'}"
/>
</group>
</div>
</div>
<div class="row">
<div class="col-6">
<group>
<field name="txt_filename" invisible="1" />
<field
name="txt_binary"
filename="txt_filename"
readonly="1"
attrs="{'invisible': [('txt_filename','=', False)]}"
/>
</group>
</div>
<div class="col-6">
<group>
<field
name="adr"
attrs="{'invisible': [('txt_filename','=', False)]}"
readonly="1"
/>
<field
name="revpar"
attrs="{'invisible': [('txt_filename','=', False)]}"
readonly="1"
/>
</group>
</div>
</div>
<div class="row">
<div class="col-6">
<button
name="ine_generate_xml"
class="btn btn-primary btn-sm"
type="object"
string="Generate INE XML"
/>
</div>
<div
class="col-6"
attrs="{'invisible': [('txt_filename','=', False)]}"
>
<b>
<a
href="https://arce.ine.es/ARCE/jsp/encuestaXml.jsp"
target="_blank"
>
Send survey to Spanish National Institute of Statistics (INE)
<i class="fa fa-signal" />
</a>
</b>
</div>
</div>
<footer />
</form>
</field>
</record>
<record id="action_ine_report" model="ir.actions.act_window">
<field name="name">Generate INE file</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pms.ine.wizard</field>
<field name="view_id" ref="pms_ine_wizard" />
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem
id="menu_ine"
name="Generate INE file"
sequence="31"
parent="pms.menu_reservations"
action="action_ine_report"
/>
</odoo>

View File

@@ -1,3 +1,4 @@
# generated from manifests external_dependencies
bs4
pycountry
xlrd