diff --git a/README.md b/README.md
index 558f865366d..370ce1457d6 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,9 @@ Available addons
addon | version | summary
--- | --- | ---
[multi_pms_properties](multi_pms_properties/) | 14.0.1.0.0 | Multi Properties Manager
-[pms](pms/) | 14.0.2.14.0 | A property management system
+[pms](pms/) | 14.0.2.15.1 | A property management system
[pms_housekeeping](pms_housekeeping/) | 14.0.1.0.1 | Housekeeping
-[pms_l10n_es](pms_l10n_es/) | 14.0.2.0.0 | PMS Spanish Adaptation
+[pms_l10n_es](pms_l10n_es/) | 14.0.2.0.1 | PMS Spanish Adaptation
[pms_rooming_xls](pms_rooming_xls/) | 14.0.1.0.0 | Rooming xlsx Management
[//]: # (end addons)
diff --git a/oca_dependencies.txt b/oca_dependencies.txt
index 1017cfa527d..880f634be15 100644
--- a/oca_dependencies.txt
+++ b/oca_dependencies.txt
@@ -2,3 +2,4 @@ partner-contact
reporting-engine
queue
connector
+community-data-files
\ No newline at end of file
diff --git a/pms/__manifest__.py b/pms/__manifest__.py
index 117f50d37f7..f9403bac028 100644
--- a/pms/__manifest__.py
+++ b/pms/__manifest__.py
@@ -4,7 +4,7 @@
{
"name": "PMS (Property Management System)",
"summary": "A property management system",
- "version": "14.0.2.14.0",
+ "version": "14.0.2.15.1",
"development_status": "Alpha",
"category": "Generic Modules/Property Management System",
"website": "https://github.com/OCA/pms",
diff --git a/pms/i18n/es.po b/pms/i18n/es.po
index 3821e86f5e5..695b0a0dcb2 100644
--- a/pms/i18n/es.po
+++ b/pms/i18n/es.po
@@ -110,7 +110,7 @@ msgstr ""
"'Order - %s' % (object.name)"
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid ", at"
msgstr ""
@@ -282,7 +282,7 @@ msgid "Checkout:"
msgstr "Total"
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Copy for the host"
msgstr ""
@@ -1176,7 +1176,7 @@ msgstr ""
#: model:ir.model.fields,field_description:pms.field_pms_property__birthdate_date
#: model:ir.model.fields,field_description:pms.field_res_partner__birthdate_date
#: model:ir.model.fields,field_description:pms.field_res_users__birthdate_date
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Birthdate"
msgstr ""
@@ -1356,7 +1356,7 @@ msgid "By Week"
msgstr "Por Semana"
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "CIF:"
msgstr ""
@@ -2700,12 +2700,12 @@ msgid "Document Type"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Document number"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Document number:"
msgstr ""
@@ -2873,7 +2873,7 @@ msgid "Enter"
msgstr "Entrada"
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Entry date"
msgstr ""
@@ -2898,7 +2898,7 @@ msgid "Exit"
msgstr "Salida"
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Exit date"
msgstr ""
@@ -2908,7 +2908,7 @@ msgid "Expedition Date"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Expedition date"
msgstr ""
@@ -3373,7 +3373,7 @@ msgstr ""
#: model:ir.model.fields,field_description:pms.field_pms_checkin_partner__gender
#: model:ir.model.fields,field_description:pms.field_pms_property__gender
#: model:ir.model.fields,field_description:pms.field_res_partner__gender
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Gender"
msgstr ""
@@ -4595,7 +4595,7 @@ msgstr ""
"crédito no conciliado o si hace click en el botón \"Listo\"."
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Lastnames"
msgstr ""
@@ -4941,7 +4941,7 @@ msgstr "Mis reservas"
#: model_terms:ir.ui.view,arch_db:pms.availability_view_form
#: model_terms:ir.ui.view,arch_db:pms.pms_room_view_form
#: model_terms:ir.ui.view,arch_db:pms.pms_shared_room_view_form
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Name"
msgstr "Nombre"
@@ -5501,7 +5501,7 @@ msgid "Overbookings"
msgstr "Overbookings"
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "PART OF TRAVELERS ENTRY"
msgstr ""
@@ -7907,7 +7907,7 @@ msgid "System Parameter"
msgstr "Parámetro del sistema"
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "TRAVELER'S DOCUMENT"
msgstr ""
@@ -8784,7 +8784,7 @@ msgid "Total amount"
msgstr "Cantidad total"
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Total amount (Reservation Card):"
msgstr ""
@@ -8872,7 +8872,7 @@ msgid "Transactions"
msgstr "Transacciones"
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Traveler's signature"
msgstr ""
@@ -8900,7 +8900,7 @@ msgstr "Martes"
#. module: pms
#: model:ir.model.fields,field_description:pms.field_pms_folio__reservation_type
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Type"
msgstr "TIpo"
diff --git a/pms/i18n/pms.pot b/pms/i18n/pms.pot
index c4be405d607..806841a451d 100644
--- a/pms/i18n/pms.pot
+++ b/pms/i18n/pms.pot
@@ -102,7 +102,7 @@ msgid ""
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid ", at"
msgstr ""
@@ -252,7 +252,7 @@ msgid "Checkout:"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Copy for the host"
msgstr ""
@@ -1088,7 +1088,7 @@ msgstr ""
#: model:ir.model.fields,field_description:pms.field_pms_property__birthdate_date
#: model:ir.model.fields,field_description:pms.field_res_partner__birthdate_date
#: model:ir.model.fields,field_description:pms.field_res_users__birthdate_date
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Birthdate"
msgstr ""
@@ -1266,7 +1266,7 @@ msgid "By Week"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "CIF:"
msgstr ""
@@ -1417,6 +1417,11 @@ msgstr ""
msgid "Channels"
msgstr ""
+#. module: pms
+#: model:ir.model.fields,field_description:pms.field_pms_reservation__check_adults
+msgid "Check Adults"
+msgstr ""
+
#. module: pms
#: model:ir.model.fields,field_description:pms.field_pms_reservation__checkin
#: model:ir.model.fields,field_description:pms.field_pms_reservation_split_join_swap_wizard__checkin
@@ -1627,6 +1632,7 @@ msgstr ""
#. module: pms
#: model:ir.model.fields,field_description:pms.field_pms_property__city
+#: model_terms:ir.ui.view,arch_db:pms.pms_property_views_form
msgid "City"
msgstr ""
@@ -1908,6 +1914,7 @@ msgstr ""
#. module: pms
#: model:ir.model.fields,field_description:pms.field_pms_property__country_id
+#: model_terms:ir.ui.view,arch_db:pms.pms_property_views_form
msgid "Country"
msgstr ""
@@ -2586,12 +2593,12 @@ msgid "Document Type"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Document number"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Document number:"
msgstr ""
@@ -2755,7 +2762,7 @@ msgid "Enter"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Entry date"
msgstr ""
@@ -2789,7 +2796,7 @@ msgid "Exit"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Exit date"
msgstr ""
@@ -2799,7 +2806,7 @@ msgid "Expedition Date"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Expedition date"
msgstr ""
@@ -3247,7 +3254,7 @@ msgstr ""
#: model:ir.model.fields,field_description:pms.field_pms_checkin_partner__gender
#: model:ir.model.fields,field_description:pms.field_pms_property__gender
#: model:ir.model.fields,field_description:pms.field_res_partner__gender
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Gender"
msgstr ""
@@ -3866,6 +3873,11 @@ msgstr ""
msgid "Internal comment for folio"
msgstr ""
+#. module: pms
+#: model:ir.model.fields,help:pms.field_pms_reservation__check_adults
+msgid "Internal field to force room capacity validations"
+msgstr ""
+
#. module: pms
#: model:ir.model.fields,help:pms.field_pms_folio__partner_internal_comment
#: model:ir.model.fields,help:pms.field_pms_reservation__partner_internal_comment
@@ -4403,7 +4415,7 @@ msgid ""
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Lastnames"
msgstr ""
@@ -4736,7 +4748,7 @@ msgstr ""
#: model_terms:ir.ui.view,arch_db:pms.availability_view_form
#: model_terms:ir.ui.view,arch_db:pms.pms_room_view_form
#: model_terms:ir.ui.view,arch_db:pms.pms_shared_room_view_form
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Name"
msgstr ""
@@ -5282,7 +5294,7 @@ msgid "Overbookings"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "PART OF TRAVELERS ENTRY"
msgstr ""
@@ -5550,13 +5562,7 @@ msgid "Persons"
msgstr ""
#. module: pms
-#: code:addons/pms/models/pms_reservation_line.py:0
-#, python-format
-msgid "Persons can't be higher than room capacity"
-msgstr ""
-
-#. module: pms
-#: code:addons/pms/models/pms_reservation.py:0
+#: code:addons/pms/models/pms_room.py:0
#, python-format
msgid "Persons can't be higher than room capacity (%s)"
msgstr ""
@@ -7500,6 +7506,7 @@ msgstr ""
#: model:ir.model.fields,field_description:pms.field_res_partner__state_id
#: model:ir.model.fields,field_description:pms.field_res_users__state_id
#: model_terms:ir.ui.view,arch_db:pms.pms_checkin_partner_view_search
+#: model_terms:ir.ui.view,arch_db:pms.pms_property_views_form
#: model_terms:ir.ui.view,arch_db:pms.pms_reservation_view_search
msgid "State"
msgstr ""
@@ -7549,6 +7556,16 @@ msgstr ""
msgid "Street"
msgstr ""
+#. module: pms
+#: model_terms:ir.ui.view,arch_db:pms.pms_property_views_form
+msgid "Street 2..."
+msgstr ""
+
+#. module: pms
+#: model_terms:ir.ui.view,arch_db:pms.pms_property_views_form
+msgid "Street..."
+msgstr ""
+
#. module: pms
#: model:ir.model.fields,field_description:pms.field_pms_property__street2
msgid "Street2"
@@ -7624,7 +7641,7 @@ msgid "System Parameter"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "TRAVELER'S DOCUMENT"
msgstr ""
@@ -8424,7 +8441,7 @@ msgid "Total amount"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Total amount (Reservation Card):"
msgstr ""
@@ -8503,7 +8520,7 @@ msgid "Transactions"
msgstr ""
#. module: pms
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Traveler's signature"
msgstr ""
@@ -8532,7 +8549,7 @@ msgstr ""
#. module: pms
#: model:ir.model.fields,field_description:pms.field_pms_booking_engine__reservation_type
#: model:ir.model.fields,field_description:pms.field_pms_folio__reservation_type
-#: model_terms:ir.ui.view,arch_db:pms.report_viajero
+#: model_terms:ir.ui.view,arch_db:pms.traveller_report
msgid "Type"
msgstr ""
@@ -8920,6 +8937,11 @@ msgstr ""
msgid "You must assign a customer name"
msgstr ""
+#. module: pms
+#: model_terms:ir.ui.view,arch_db:pms.pms_property_views_form
+msgid "ZIP"
+msgstr ""
+
#. module: pms
#: model:ir.model.fields,field_description:pms.field_pms_property__zip
msgid "Zip"
diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py
index 38a3e006054..b24fc967550 100644
--- a/pms/models/pms_reservation.py
+++ b/pms/models/pms_reservation.py
@@ -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 @@ def check_consecutive_dates(self):
# _("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 @@ def _no_agency_as_agency(self):
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 @@ def create(self, vals):
)
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()
diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py
index 3d5e2b641b7..6331997fae4 100644
--- a/pms/models/pms_reservation_line.py
+++ b/pms/models/pms_reservation_line.py
@@ -470,17 +470,3 @@ def constrains_duplicated_date(self):
)
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"))
diff --git a/pms/models/pms_room.py b/pms/models/pms_room.py
index e83742eac7b..dcb6869a052 100644
--- a/pms/models/pms_room.py
+++ b/pms/models/pms_room.py
@@ -118,6 +118,26 @@ def _check_capacity(self):
)
)
+ @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):
diff --git a/pms/models/pms_service.py b/pms/models/pms_service.py
index 858c0cfa72b..af621271ae6 100644
--- a/pms/models/pms_service.py
+++ b/pms/models/pms_service.py
@@ -340,23 +340,24 @@ def _compute_service_line_ids(self):
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 @@ def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id):
# 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
diff --git a/pms/report/traveller_report_action.xml b/pms/report/traveller_report_action.xml
index 17388a536d2..f04c2bbdfb5 100644
--- a/pms/report/traveller_report_action.xml
+++ b/pms/report/traveller_report_action.xml
@@ -5,8 +5,8 @@
Traveller Report
pms.checkin.partner
qweb-pdf
- pms.report_viajero
- pms.report_viajero
+ pms.traveller_report
+ pms.traveller_report
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pms/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml
index af3fd4b9a6c..7fbe5382514 100644
--- a/pms/views/pms_reservation_views.xml
+++ b/pms/views/pms_reservation_views.xml
@@ -16,6 +16,7 @@
+
diff --git a/pms_l10n_es/views/pms_room_views.xml b/pms_l10n_es/views/pms_room_views.xml
new file mode 100644
index 00000000000..820fceaa943
--- /dev/null
+++ b/pms_l10n_es/views/pms_room_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ Property Form l10n_es
+ pms.room
+
+
+
+
+
+
+
+
diff --git a/pms_l10n_es/wizards/__init__.py b/pms_l10n_es/wizards/__init__.py
index 1a18223f30b..52c12cbc4aa 100644
--- a/pms_l10n_es/wizards/__init__.py
+++ b/pms_l10n_es/wizards/__init__.py
@@ -1 +1,2 @@
from . import traveller_report
+from . import wizard_ine
diff --git a/pms_l10n_es/wizards/wizard_ine.py b/pms_l10n_es/wizards/wizard_ine.py
new file mode 100644
index 00000000000..416ddff22bd
--- /dev/null
+++ b/pms_l10n_es/wizards/wizard_ine.py
@@ -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 = ''
+ 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",
+ }
diff --git a/pms_l10n_es/wizards/wizard_ine.xml b/pms_l10n_es/wizards/wizard_ine.xml
new file mode 100644
index 00000000000..120f3ca139b
--- /dev/null
+++ b/pms_l10n_es/wizards/wizard_ine.xml
@@ -0,0 +1,102 @@
+
+
+
+ INE
+ pms.ine.wizard
+
+
+
+
+
+
+ Generate INE file
+ ir.actions.act_window
+ pms.ine.wizard
+
+ form
+ new
+
+
+
diff --git a/requirements.txt b/requirements.txt
index 3e0e636687b..1fb3c55d7a2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
# generated from manifests external_dependencies
bs4
+pycountry
xlrd