[IMP] pms_l10n_es: new mandatory institution to send reservations & guest info communications

This commit is contained in:
miguelpadin
2024-06-07 23:56:49 +01:00
parent 62b7274705
commit 899abc2395
21 changed files with 16162 additions and 91 deletions

View File

@@ -438,7 +438,7 @@ class PmsCheckinPartner(models.Model):
elif not record.residence_state_id:
record.residence_state_id = False
@api.depends(lambda self: self._checkin_manual_fields(depends=True))
@api.depends(lambda self: self._get_depends_state_fields())
def _compute_state(self):
for record in self:
if not record.state:
@@ -454,7 +454,8 @@ class PmsCheckinPartner(models.Model):
elif any(
not getattr(record, field)
for field in record._checkin_mandatory_fields(
country=record.document_country_id
residence_country=record.residence_country_id,
document_type=record.document_type,
)
):
record.state = "draft"
@@ -783,7 +784,7 @@ class PmsCheckinPartner(models.Model):
return res
@api.model
def _checkin_manual_fields(self, country=False, depends=False):
def _checkin_manual_fields(self, country=False):
manual_fields = [
"name",
"partner_id",
@@ -805,20 +806,19 @@ class PmsCheckinPartner(models.Model):
"residence_country_id",
"residence_state_id",
]
# api.depends need "reservation_id.state" in the lambda function
if depends:
manual_fields.append("reservation_id.state")
return manual_fields
@api.model
def _checkin_mandatory_fields(self, country=False, depends=False):
def _get_depends_state_fields(self):
manual_fields = self._checkin_manual_fields()
manual_fields.append("reservation_id.state")
return manual_fields
@api.model
def _checkin_mandatory_fields(self, residence_country=False, document_type=False):
mandatory_fields = [
"name",
]
# api.depends need "reservation_id.state" in the lambda function
if depends:
mandatory_fields.extend(["reservation_id.state", "name"])
return mandatory_fields
@api.model
@@ -888,7 +888,11 @@ class PmsCheckinPartner(models.Model):
raise ValidationError(_("Its too late to checkin"))
if any(
not getattr(record, field) for field in self._checkin_mandatory_fields()
not getattr(record, field)
for field in self._checkin_mandatory_fields(
residence_country=record.residence_country_id,
document_type=record.document_type,
)
):
raise ValidationError(_("Personal data is missing for check-in"))
vals = {

View File

@@ -2117,7 +2117,8 @@ class PmsReservation(models.Model):
else:
raise ValidationError(
_(
"The Property and Sale Channel Origin are mandatory in the reservation"
"The Property, Sale Channel Origin "
"and name / partner name / agency are mandatory in the reservation"
)
)
if vals.get("name", _("New")) == _("New") or "name" not in vals:

View File

@@ -30,6 +30,7 @@
"data": [
"data/res.country.state.csv",
"data/pms.ine.tourism.type.category.csv",
# "data/pms.ine.zip.municipality.ine.relation.csv",
"data/cron_jobs.xml",
"data/pms_data.xml",
"data/queue_data.xml",
@@ -39,9 +40,11 @@
"views/pms_property_views.xml",
"views/pms_room_views.xml",
"views/pms_log_institution_traveller_report_views.xml",
"views/pms_ses_communication_views.xml",
"views/pms_ine_tourism_type_category.xml",
"views/res_partner_id_number_view.xml",
"views/pms_checkin_partner_views.xml",
"views/pms_reservation_views.xml",
"wizards/traveller_report.xml",
"wizards/wizard_ine.xml",
"reports/invoice.xml",

View File

@@ -16,4 +16,81 @@
/>
<field name="code">model.send_file_institution_async()</field>
</record>
<record model="ir.cron" id="auto_creation_traveller_communications">
<field name="name">Automatic Creation Traveller Communications</field>
<field name="active" eval="False" />
<field name="interval_number">3</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="state">code</field>
<field name="model_id" ref="model_traveller_report_wizard" />
<field
name="nextcall"
eval="datetime.now(pytz.timezone('UTC')).strftime('%Y-%m-%d 16:57:00')"
/>
<field
name="code"
>model.create_pending_notifications_traveller_report()</field>
</record>
<record model="ir.cron" id="auto_sending_pending_reservation_communications">
<field
name="name"
>Automatic Sending Pending Reservation Communications</field>
<field name="active" eval="False" />
<field name="interval_number">3</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="state">code</field>
<field name="model_id" ref="model_traveller_report_wizard" />
<field
name="nextcall"
eval="datetime.now(pytz.timezone('UTC')).strftime('%Y-%m-%d 16:57:00')"
/>
<field name="code">model.ses_send_communications('RH')</field>
</record>
<record
model="ir.cron"
id="auto_sending_pending_traveller_reports_communications"
>
<field
name="name"
>Automatic Sending Pending Traveller Reports Communications</field>
<field name="active" eval="False" />
<field name="interval_number">3</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="state">code</field>
<field name="model_id" ref="model_traveller_report_wizard" />
<field
name="nextcall"
eval="datetime.now(pytz.timezone('UTC')).strftime('%Y-%m-%d 16:57:00')"
/>
<field name="code">model.ses_send_communications('PV')</field>
</record>
<record model="ir.cron" id="auto_processing_sent_communications">
<field name="name">Automatic Process Sent Communications</field>
<field name="active" eval="False" />
<field name="interval_number">3</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="state">code</field>
<field name="model_id" ref="model_traveller_report_wizard" />
<field
name="nextcall"
eval="datetime.now(pytz.timezone('UTC')).strftime('%Y-%m-%d 16:57:00')"
/>
<field name="code">model.ses_process_communications()</field>
</record>
</odoo>

View File

@@ -8,3 +8,5 @@ from . import pms_room
from . import res_partner
from . import pms_checkin_partner
from . import res_partner_id_number
from . import pms_ses_communication
from . import pms_reservation

View File

@@ -3,6 +3,8 @@ import logging
from odoo import api, fields, models
CODE_SPAIN = "ES"
CODE_NIF = "D"
CODE_NIE = "N"
_logger = logging.getLogger(__name__)
@@ -34,9 +36,9 @@ class PmsCheckinPartner(models.Model):
record.support_number = False
@api.model
def _checkin_mandatory_fields(self, country=False, depends=False):
def _checkin_mandatory_fields(self, residence_country=False, document_type=False):
mandatory_fields = super(PmsCheckinPartner, self)._checkin_mandatory_fields(
depends
residence_country, document_type
)
mandatory_fields.extend(
[
@@ -46,20 +48,35 @@ class PmsCheckinPartner(models.Model):
"document_type",
"document_expedition_date",
"nationality_id",
"residence_street",
"residence_city",
"residence_country_id",
"residence_zip",
]
)
if depends or (country and country.code == CODE_SPAIN):
if residence_country and residence_country.code == CODE_SPAIN:
mandatory_fields.extend(
[
"residence_state_id",
"residence_street",
"residence_city",
]
)
if document_type.code and document_type.code == CODE_NIF:
mandatory_fields.extend(
[
"lastname2",
]
)
if document_type and document_type.code in [CODE_NIF, CODE_NIE]:
mandatory_fields.extend(
[
"support_number",
]
)
return mandatory_fields
@api.model
def _checkin_manual_fields(self, country=False, depends=False):
manual_fields = super(PmsCheckinPartner, self)._checkin_manual_fields(depends)
def _checkin_manual_fields(self, country=False):
manual_fields = super(PmsCheckinPartner, self)._checkin_manual_fields()
manual_fields.extend(["support_number"])
return manual_fields

View File

@@ -17,6 +17,7 @@ class PmsProperty(models.Model):
("policia_nacional", "Policía Nacional"),
("ertxaintxa", "Ertxaintxa (soon)"),
("mossos", "Mossos_d'esquadra (soon)"),
("ses", "SES"),
],
string="Institution",
help="Institution to send daily guest data.",
@@ -26,6 +27,10 @@ class PmsProperty(models.Model):
string="Institution property id",
help="Id provided by institution to send data from property.",
)
ses_url = fields.Char(
string="SES URL",
help="URL to send the data to SES",
)
institution_user = fields.Char(
string="Institution user", help="User provided by institution to send the data."
)
@@ -33,6 +38,11 @@ class PmsProperty(models.Model):
string="Institution password",
help="Password provided by institution to send the data.",
)
institution_lessor_id = fields.Char(
string="Institution lessor id",
help="Id provided by institution to send data from lessor.",
)
ine_tourism_number = fields.Char(
"Tourism number",
help="Registration number in the Ministry of Tourism. Used for INE statistics.",

View File

@@ -0,0 +1,88 @@
from odoo import api, fields, models
class PmsReservation(models.Model):
_inherit = "pms.reservation"
ses_communication_ids = fields.One2many(
string="SES Communications",
help="Communications related to this reservation",
comodel_name="pms.ses.communication",
inverse_name="reservation_id",
)
is_ses = fields.Boolean(
string="Is SES",
readonly=True,
compute="_compute_is_ses",
)
@api.depends("pms_property_id")
def _compute_is_ses(self):
for record in self:
record.is_ses = record.pms_property_id.institution == "ses"
@api.model
def create_communication(self, reservation_id, operation, entity):
self.env["pms.ses.communication"].create(
{
"reservation_id": reservation_id,
"operation": operation,
"entity": entity,
}
)
@api.model
def create(self, vals):
reservation = super(PmsReservation, self).create(vals)
if reservation.pms_property_id.institution == "ses":
self.create_communication(reservation.id, "A", "RH")
return reservation
@api.model
def create_communication_after_update_reservation(self, reservation, vals):
state_changed = "state" in vals and (
(vals["state"] != "cancel" and reservation.state == "cancel")
or (vals["state"] == "cancel" and reservation.state != "cancel")
)
check_changed = (
any(
key in vals and vals[key] != getattr(reservation, key)
for key in ["adults", "checkin", "checkout"]
)
and reservation.state != "cancel"
)
if state_changed or check_changed:
# delete all pending notifications
self.env["pms.ses.communication"].search(
[
("reservation_id", "=", reservation.id),
("state", "=", "to_send"),
("entity", "=", "RH"),
]
).unlink()
# last communication
last_communication = self.env["pms.ses.communication"].search(
[
("reservation_id", "=", reservation.id),
("entity", "=", "RH"),
],
order="id desc",
limit=1,
)
if state_changed:
if vals["state"] == "cancel" and last_communication.operation == "A":
self.create_communication(reservation.id, "B", "RH")
elif vals["state"] != "cancel" and last_communication.operation == "B":
self.create_communication(reservation.id, "A", "RH")
elif check_changed:
if last_communication.operation == "A":
self.create_communication(reservation.id, "B", "RH")
self.create_communication(reservation.id, "A", "RH")
def write(self, vals):
for record in self:
if record.pms_property_id.institution == "ses":
self.create_communication_after_update_reservation(record, vals)
return super(PmsReservation, self).write(vals)

View File

@@ -0,0 +1,86 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class PmsSesCommunication(models.Model):
_name = "pms.ses.communication"
_description = "SES Communication"
reservation_id = fields.Many2one(
string="Reservation",
help="Reservation related to this communication",
index=True,
required=True,
comodel_name="pms.reservation",
)
communication_id = fields.Char(
string="Communication ID",
help="ID of the communication",
default=False,
)
operation = fields.Selection(
string="Operation",
help="Operation of the communication",
selection=[("A", "New communication"), ("B", "Delete communication")],
required=True,
)
entity = fields.Selection(
string="Entity",
help="Entity of the communication",
selection=[("RH", "Reservation"), ("PV", "Traveller report")],
required=True,
)
communication_time = fields.Datetime(
string="Communication time",
help="Date and time of the communication",
)
query_status_time = fields.Datetime(
string="Query status time",
help="Date and time of the last state query",
)
state = fields.Selection(
string="State",
help="State of the communication",
default="to_send",
required=True,
selection=[
("to_send", "Pending Notification"),
("to_process", "Pending Processing"),
("error_sending", "Error Sending"),
("error_processing", "Error Processing"),
("processed", "Processed"),
],
)
sending_result = fields.Text(
string="Sending Result",
help="Notification sending result",
)
processing_result = fields.Text(
string="Processing Result",
help="Notification processing result",
)
communication_xml = fields.Text(
string="XML Com.",
help="XML content communication",
)
communication_soap = fields.Text(
string="SOAP Com.",
help="SOAP content communication",
)
response_communication_soap = fields.Text(
string="SOAP Resp. Com.",
help="SOAP response communication",
)
query_status_xml = fields.Text(
string="XML Query Status",
help="XML query status content communication",
)
query_status_soap = fields.Text(
string="SOAP Query Status",
help="SOAP query status content communication",
)
response_query_status_soap = fields.Text(
string="SOAP Resp. Status",
help="SOAP response status query",
)

View File

@@ -3,3 +3,4 @@ user_access_traveller_report_wizard,user_access_traveller_report_wizard,model_tr
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
user_access_pms_ses_communication,user_access_pms_ses_communication,model_pms_ses_communication,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
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
6 user_access_pms_ses_communication user_access_pms_ses_communication model_pms_ses_communication pms.group_pms_user 1 1 1 1

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
from . import test_wizard_ine
from . import test_res_partner
from . import test_wizard_traveller_report
from . import test_pms_ses_communication

View File

@@ -0,0 +1,285 @@
from odoo import fields
from odoo.tools.safe_eval import datetime
from .common import TestPms
class TestPmsSesCommunication(TestPms):
def setUp(self):
super().setUp()
self.sale_channel_direct1 = self.env["pms.sale.channel"].create(
{
"name": "Door",
"channel_type": "direct",
}
)
# create room type
self.room_type = self.env["pms.room.type"].create(
{
"name": "Room type test",
"default_code": "DBL_Test",
"class_id": self.room_type_class1.id,
}
)
# room
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.pms_property1.institution = "ses"
def test_create_notification_when_create_reservation(self):
# ARRANGE/ACT
reservation = self.env["pms.reservation"].create(
{
"pms_property_id": self.pms_property1.id,
"room_type_id": self.room_type.id,
"checkin": "2021-01-01",
"checkout": "2021-01-02",
"adults": 2,
"children": 0,
"sale_channel_origin_id": self.sale_channel_direct1.id,
"partner_name": "Test reservation",
}
)
# ASSERT
last_notification = self.env["pms.ses.communication"].search(
[
("reservation_id", "=", reservation.id),
]
)
self.assertEqual(
last_notification.operation,
"A",
"Creating a reservation should create a notification with operation A",
)
def test_not_create_notification_when_cancel_reservation_and_not_sent(self):
# ARRANGE
reservation = self.env["pms.reservation"].create(
{
"pms_property_id": self.pms_property1.id,
"room_type_id": self.room_type.id,
"checkin": fields.date.today() + datetime.timedelta(days=1),
"checkout": fields.date.today() + datetime.timedelta(days=2),
"adults": 2,
"children": 0,
"sale_channel_origin_id": self.sale_channel_direct1.id,
"partner_name": "Test reservation",
}
)
# ACT
reservation.action_cancel()
# ASSERT
last_notifications = self.env["pms.ses.communication"].search(
[
("reservation_id", "=", reservation.id),
],
order="id",
)
self.assertFalse(
last_notifications,
"Cancelling a reservation not sent should not create a notification",
)
def test_create_notification_when_cancel_reservation_and_is_sent(self):
# ARRANGE
reservation = self.env["pms.reservation"].create(
{
"pms_property_id": self.pms_property1.id,
"room_type_id": self.room_type.id,
"checkin": fields.date.today() + datetime.timedelta(days=1),
"checkout": fields.date.today() + datetime.timedelta(days=2),
"adults": 2,
"children": 0,
"sale_channel_origin_id": self.sale_channel_direct1.id,
"partner_name": "Test reservation",
}
)
notification_after_create_reservation = self.env[
"pms.ses.communication"
].search(
[
("reservation_id", "=", reservation.id),
("operation", "=", "A"),
]
)
notification_after_create_reservation.state = "to_process"
# ACT
reservation.action_cancel()
# ASSERT
last_notifications = (
self.env["pms.ses.communication"]
.search(
[
("reservation_id", "=", reservation.id),
],
order="id",
)
.mapped("operation")
)
self.assertEqual(
last_notifications,
["A", "B"],
"Canceling a reservation should create a notification with operation B",
)
def test_create_notification_when_modify_reservation_and_not_sent(self):
# ARRANGE
update_operations = [
{
"adults": 1,
},
{
"checkin": fields.date.today() + datetime.timedelta(days=10),
},
{
"checkout": fields.date.today() + datetime.timedelta(days=12),
},
]
reservation = self.env["pms.reservation"].create(
{
"pms_property_id": self.pms_property1.id,
"room_type_id": self.room_type.id,
"checkin": fields.date.today() + datetime.timedelta(days=1),
"checkout": fields.date.today() + datetime.timedelta(days=13),
"adults": 2,
"children": 0,
"sale_channel_origin_id": self.sale_channel_direct1.id,
"partner_name": "Test reservation",
}
)
# ACT & ASSERT
for _index, update_operation in enumerate(update_operations):
with self.subTest(k=update_operation):
reservation.write(update_operation)
last_notification_operations = (
self.env["pms.ses.communication"]
.search(
[
("reservation_id", "=", reservation.id),
],
order="id",
)
.mapped("operation")
)
self.assertEqual(
["A"],
last_notification_operations,
"Update adults should create 2 notifications with operations A and B",
)
def test_create_notification_when_modify_reservation_and_is_sent(self):
# ARRANGE
update_operations = [
{
"adults": 1,
},
{
"checkin": fields.date.today() + datetime.timedelta(days=10),
},
{
"checkout": fields.date.today() + datetime.timedelta(days=12),
},
]
reservation = self.env["pms.reservation"].create(
{
"pms_property_id": self.pms_property1.id,
"room_type_id": self.room_type.id,
"checkin": fields.date.today() + datetime.timedelta(days=1),
"checkout": fields.date.today() + datetime.timedelta(days=13),
"adults": 2,
"children": 0,
"sale_channel_origin_id": self.sale_channel_direct1.id,
"partner_name": "Test reservation",
}
)
reservation_communications = self.env["pms.ses.communication"].search(
[("reservation_id", "=", reservation.id)]
)
reservation_communications.state = "to_process"
# ACT & ASSERT
for _index, update_operation in enumerate(update_operations):
with self.subTest(k=update_operation):
reservation.write(update_operation)
reservation_communications = (
self.env["pms.ses.communication"]
.search(
[
("reservation_id", "=", reservation.id),
],
order="id",
)
.mapped("operation")
)
self.assertEqual(
["A", "B", "A"],
reservation_communications,
"Update adults should create 2 notifications with operations A and B",
)
def test_create_notification_when_checkin_partner_on_board(self):
# ARRANGE
partner = self.env["res.partner"].create(
{
"name": "name test",
"firstname": "firstname test",
"lastname": "lastname test",
"lastname2": "lastname2 test",
"birthdate_date": "1995-12-10",
"gender": "male",
"nationality_id": self.env.ref("base.es").id,
"residence_street": "street test",
"residence_city": "city test",
"residence_zip": "zip test",
"residence_country_id": self.env.ref("base.us").id,
}
)
reservation = self.env["pms.reservation"].create(
{
"pms_property_id": self.pms_property1.id,
"room_type_id": self.room_type.id,
"checkin": fields.date.today(),
"checkout": fields.date.today() + datetime.timedelta(days=13),
"adults": 1,
"children": 0,
"sale_channel_origin_id": self.sale_channel_direct1.id,
"partner_name": "Test reservation",
}
)
document_type_dni = self.env["res.partner.id_category"].search(
[("code", "=", "D")], limit=1
)
checkin_partner = self.env["pms.checkin.partner"].create(
{
"reservation_id": reservation.id,
"partner_id": partner.id,
"document_number": "11111111H",
"document_type": document_type_dni.id,
"document_expedition_date": fields.date.today()
+ datetime.timedelta(days=1),
"support_number": "123456",
}
)
checkin_partner.action_on_board()
# ACT
self.env[
"traveller.report.wizard"
].create_pending_notifications_traveller_report()
# ASSERT
last_notification = self.env["pms.ses.communication"].search(
[
("reservation_id", "=", reservation.id),
("operation", "=", "A"),
("state", "=", "to_send"),
("entity", "=", "PV"),
],
)
self.assertTrue(
last_notification,
"Notification should be created when checkin partner is on board",
)

View File

@@ -250,6 +250,9 @@ class TestWizardINE(TestPms):
{
"partner_id": self.partner_1.id,
"reservation_id": self.reservation_1.id,
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
@@ -257,6 +260,9 @@ class TestWizardINE(TestPms):
{
"partner_id": self.partner_2.id,
"reservation_id": self.reservation_1.id,
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
# Create reservation 2
@@ -275,12 +281,18 @@ class TestWizardINE(TestPms):
{
"partner_id": self.partner_3.id,
"reservation_id": self.reservation_2.id,
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
self.checkin4 = self.env["pms.checkin.partner"].create(
{
"partner_id": self.partner_4.id,
"reservation_id": self.reservation_2.id,
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
# Create reservation 3
@@ -299,6 +311,9 @@ class TestWizardINE(TestPms):
{
"partner_id": self.partner_5.id,
"reservation_id": self.reservation_3.id,
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
# Create reservation property 2
@@ -317,6 +332,9 @@ class TestWizardINE(TestPms):
{
"partner_id": self.partner_5.id,
"reservation_id": self.reservation_property_2.id,
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
@@ -350,6 +368,9 @@ class TestWizardINE(TestPms):
{
"partner_id": self.partner_6.id,
"reservation_id": self.reservation_4.id,
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
@@ -357,6 +378,9 @@ class TestWizardINE(TestPms):
{
"partner_id": self.partner_7.id,
"reservation_id": self.reservation_4.id,
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
# checkin partners on board
@@ -387,6 +411,9 @@ class TestWizardINE(TestPms):
"residence_country_id": self.country_russia.id,
"birthdate_date": "2000-06-25",
"gender": "male",
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
self.partner_russia_2 = self.env["res.partner"].create(
@@ -397,6 +424,9 @@ class TestWizardINE(TestPms):
"residence_country_id": self.country_russia.id,
"birthdate_date": "2000-06-25",
"gender": "male",
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
self.partner_russia_3 = self.env["res.partner"].create(
@@ -407,6 +437,9 @@ class TestWizardINE(TestPms):
"residence_country_id": self.country_russia.id,
"birthdate_date": "2000-06-25",
"gender": "male",
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
# Create document for 3 checkin partners (russia)
@@ -670,38 +703,38 @@ class TestWizardINE(TestPms):
[("name", "=", "Pontevedra")]
)
self.checkin1.nationality_id = country_spain
self.partner_1.nationality_id = country_spain
self.checkin1.residence_country_id = country_spain
self.partner_1.residence_country_id = country_spain
self.checkin1.residence_state_id = state_ourense
self.partner_1.residence_state_id = state_ourense
self.checkin2.nationality_id = country_spain
self.partner_2.nationality_id = country_spain
self.checkin2.residence_country_id = country_spain
self.partner_2.residence_country_id = country_spain
self.checkin2.residence_state_id = state_pontevedra
self.partner_2.residence_state_id = state_pontevedra
self.checkin3.nationality_id = country_spain
self.partner_3.nationality_id = country_spain
self.checkin3.residence_country_id = country_spain
self.partner_3.residence_country_id = country_spain
self.checkin3.residence_state_id = state_ourense
self.partner_3.residence_state_id = state_ourense
self.checkin4.nationality_id = country_spain
self.partner_4.nationality_id = country_spain
self.checkin4.residence_country_id = country_spain
self.partner_4.residence_country_id = country_spain
self.checkin4.residence_state_id = state_ourense
self.partner_4.residence_state_id = state_ourense
self.checkin5.nationality_id = country_spain
self.partner_5.nationality_id = country_spain
self.checkin5.residence_country_id = country_spain
self.partner_5.residence_country_id = country_spain
self.checkin5.residence_state_id = state_madrid
self.partner_5.residence_state_id = state_madrid
self.checkin6.nationality_id = country_spain
self.partner_6.nationality_id = country_spain
self.checkin6.residence_country_id = country_spain
self.partner_6.residence_country_id = country_spain
self.checkin6.residence_state_id = state_madrid
self.partner_6.residence_state_id = state_madrid
self.checkin7.nationality_id = country_spain
self.partner_7.nationality_id = country_spain
self.checkin7.residence_country_id = country_spain
self.partner_7.residence_country_id = country_spain
self.checkin7.residence_state_id = state_madrid
self.partner_7.residence_state_id = state_madrid
@@ -914,7 +947,7 @@ class TestWizardINE(TestPms):
"""
# ARRANGE
self.ideal_scenario()
self.reservation_1.checkin_partner_ids[1].nationality_id = False
self.reservation_1.checkin_partner_ids[1].residence_country_id = False
start_date = datetime.date(2021, 2, 1)
end_date = datetime.date(2021, 2, 4)
@@ -969,38 +1002,38 @@ class TestWizardINE(TestPms):
[("name", "=", "Ourense (Orense)")]
)
self.checkin1.nationality_id = country_spain
self.partner_1.nationality_id = country_spain
self.checkin1.residence_country_id = country_spain
self.partner_1.residence_country_id = country_spain
self.checkin1.residence_state_id = state_ourense
self.partner_1.residence_state_id = state_ourense
self.checkin2.nationality_id = country_spain
self.partner_2.nationality_id = country_spain
self.checkin2.residence_country_id = country_spain
self.partner_2.residence_country_id = country_spain
self.checkin2.residence_state_id = False
self.partner_2.residence_state_id = False
self.checkin3.nationality_id = country_spain
self.partner_3.nationality_id = country_spain
self.checkin3.residence_country_id = country_spain
self.partner_3.residence_country_id = country_spain
self.checkin3.residence_state_id = state_ourense
self.partner_3.residence_state_id = state_ourense
self.checkin4.nationality_id = country_spain
self.partner_4.nationality_id = country_spain
self.checkin4.residence_country_id = country_spain
self.partner_4.residence_country_id = country_spain
self.checkin4.residence_state_id = state_ourense
self.partner_4.residence_state_id = state_ourense
self.checkin5.nationality_id = country_spain
self.partner_5.nationality_id = country_spain
self.checkin5.residence_country_id = country_spain
self.partner_5.residence_country_id = country_spain
self.checkin5.residence_state_id = state_madrid
self.partner_5.residence_state_id = state_madrid
self.checkin6.nationality_id = country_spain
self.partner_6.nationality_id = country_spain
self.checkin6.residence_country_id = country_spain
self.partner_6.residence_country_id = country_spain
self.checkin6.residence_state_id = state_madrid
self.partner_6.residence_state_id = state_madrid
self.checkin7.nationality_id = country_spain
self.partner_7.nationality_id = country_spain
self.checkin7.residence_country_id = country_spain
self.partner_7.residence_country_id = country_spain
self.checkin7.residence_state_id = state_madrid
self.partner_7.residence_state_id = state_madrid

View File

@@ -124,6 +124,9 @@ class TestWizardTravellerReport(TestPms):
"reservation_id": self.reservation_1.id,
"firstname": "John",
"lastname": "Doe",
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
# Create reservation 2
@@ -144,6 +147,9 @@ class TestWizardTravellerReport(TestPms):
"reservation_id": self.reservation_2.id,
"firstname": "Martha",
"lastname": "Stewart",
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
# checkin partners on board
@@ -188,6 +194,9 @@ class TestWizardTravellerReport(TestPms):
"firstname": "John",
"lastname": "Doe",
"nationality_id": self.country_italy.id,
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
# Create reservation 2
@@ -208,6 +217,9 @@ class TestWizardTravellerReport(TestPms):
"reservation_id": self.reservation_2.id,
"firstname": "Martha",
"lastname": "Stewart",
"residence_street": "Test street 1",
"residence_city": "Test city",
"residence_zip": "08001",
}
)
# checkin partners on board

View File

@@ -18,7 +18,12 @@
<div class="col-6">
<group name="property_data">
<field name="institution" />
<field name="institution_lessor_id" />
<field name="institution_property_id" />
<field
name="ses_url"
attrs="{'invisible': [('institution','!=','ses')]}"
/>
</group>
</div>
<div class="col-6 px-0">
@@ -31,6 +36,7 @@
class="btn btn-primary btn-sm"
type="object"
string="Test user/password"
attrs="{'invisible': [('institution','=','ses')]}"
/>
</div>
</div>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_pms_reservation_from_pms_l110n_es" model="ir.ui.view">
<field name="name">Rerservation Form l10n_es</field>
<field name="model">pms.reservation</field>
<field name="inherit_id" ref="pms.pms_reservation_view_form" />
<field name="arch" type="xml">
<xpath expr="//page[@name='others']" position="before">
<page
string="SES Communications"
name="property_general"
attrs="{'invisible': [('is_ses','!=',True),'|',('is_ses','=',True),('ses_communication_ids','!=',False)]}"
>
<field name="is_ses" invisible="1" />
<field name="ses_communication_ids">
<tree string="Comunicaciones">
<field name="reservation_id" />
<field name="communication_id" />
<field name="operation" />
<field name="entity" />
<!-- times -->
<field name="create_date" />
<field name="communication_time" />
<field name="query_status_time" />
<field name="state" />
<field name="sending_result" />
<field name="processing_result" />
</tree>
</field>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="pms_ses_communication_view_form">
<field name="name">pms.ses.communication.form</field>
<field name="model">pms.ses.communication</field>
<field name="arch" type="xml">
<form string="Log institution traveller report detail">
<sheet>
<group>
<field name="reservation_id" />
<field name="communication_id" />
<field name="operation" />
<field name="entity" />
<!-- times -->
<field name="create_date" />
<field name="communication_time" />
<field name="query_status_time" />
<!-- results -->
<field name="state" />
<field name="sending_result" />
<field name="processing_result" />
<!-- communication soap, xml (request) & soap (response) -->
<field name="communication_xml" widget="CopyClipboardChar" />
<field name="communication_soap" widget="CopyClipboardChar" />
<field
name="response_communication_soap"
widget="CopyClipboardChar"
/>
<!-- processing soap, xml (request) & soap (response) -->
<field name="query_status_xml" widget="CopyClipboardChar" />
<field name="query_status_soap" widget="CopyClipboardChar" />
<field
name="response_query_status_soap"
widget="CopyClipboardChar"
/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="pms_ses_communication_view_tree">
<field name="name">pms.ses.communication.tree</field>
<field name="model">pms.ses.communication</field>
<field name="arch" type="xml">
<tree name="Property Ubications" create="false">
<field name="reservation_id" />
<field name="communication_id" />
<field name="operation" />
<field name="entity" />
<!-- times -->
<field name="create_date" />
<field name="communication_time" />
<field name="query_status_time" />
<!-- results -->
<field name="state" />
<field name="sending_result" />
<field name="processing_result" />
</tree>
</field>
</record>
<record model="ir.ui.view" id="pms_ses_communication_view_search">
<field name="name">pms.ses.communication.search</field>
<field name="model">pms.ses.communication</field>
<field name="arch" type="xml">
<search string="Log SES Communications">
<field name="communication_id" />
<field name="reservation_id" />
<!-- times -->
<field name="create_date" />
<field name="communication_time" />
<field name="query_status_time" />
<field name="state" />
</search>
</field>
</record>
<record model="ir.actions.act_window" id="open_pms_ses_communication_form_tree">
<field name="name">SES Communications</field>
<field name="res_model">pms.ses.communication</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
name="SES Communications"
id="menu_open_pms_ses_communication_form_tree"
action="open_pms_ses_communication_form_tree"
parent="pms.menu_reservations"
sequence="29"
/>
</odoo>

View File

@@ -1,13 +1,18 @@
import base64
import csv
import datetime
import io
import json
import logging
import re
import time
import xml.etree.cElementTree as ET
import zipfile
import requests
from bs4 import BeautifulSoup as bs
from dateutil.relativedelta import relativedelta
from requests.packages.urllib3.exceptions import InsecureRequestWarning
from odoo import _, api, fields, models
from odoo.exceptions import MissingError, ValidationError
@@ -15,6 +20,305 @@ from odoo.modules.module import get_module_resource
_logger = logging.getLogger(__name__)
CODE_SPAIN = "ES"
CODE_PASSPORT = "P"
CODE_DNI = "D"
CODE_NIE = "N"
REQUEST_CODE_OK = "0"
XML_OK = "1"
XML_PROCESSING = "4"
XML_PENDING = "5"
DELETE_OPERATION_CODE = "B"
# Disable insecure request warnings
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
def _string_to_zip_to_base64(string_data):
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
zip_file.writestr("data.xml", string_data.encode("utf-8"))
zip_buffer.seek(0)
zip_data = zip_buffer.read()
zip_base64 = base64.b64encode(zip_data)
return zip_base64.decode()
def _ses_xml_payment_elements(contrato, reservation):
pago = ET.SubElement(contrato, "pago")
payments = reservation.folio_id.payment_ids.filtered(lambda x: x.state == "posted")
tipo_pago = "DESTI"
if payments:
payment = payments[0]
tipo_pago = "EFECT" if payment.journal_id.type == "cash" else "PLATF"
ET.SubElement(pago, "tipoPago").text = tipo_pago
def _ses_xml_contract_elements(comunicacion, reservation):
contrato = ET.SubElement(comunicacion, "contrato")
ET.SubElement(contrato, "referencia").text = reservation.name
ET.SubElement(contrato, "fechaContrato").text = str(reservation.date_order)[:10]
ET.SubElement(
contrato, "fechaEntrada"
).text = f"{str(reservation.checkin)[:10]}T00:00:00"
ET.SubElement(
contrato, "fechaSalida"
).text = f"{str(reservation.checkout)[:10]}T00:00:00"
ET.SubElement(contrato, "numPersonas").text = str(reservation.adults)
_ses_xml_payment_elements(contrato, reservation)
def _ses_xml_text_element_and_validate(parent, tag, text, error_message):
if text:
ET.SubElement(parent, tag).text = text
else:
raise ValidationError(error_message)
def _ses_xml_map_document_type(code):
if code == CODE_DNI:
return "NIF"
elif code == CODE_NIE:
return "NIE"
elif code == CODE_PASSPORT:
return "PAS"
else:
return "OTRO"
def _ses_xml_person_names_elements(persona, reservation, checkin_partner):
if reservation:
name = False
if reservation.partner_id.firstname:
name = reservation.partner_id.firstname
elif reservation.partner_name:
name = reservation.partner_name.split(" ")[0]
_ses_xml_text_element_and_validate(
persona,
"nombre",
name,
_("The reservation does not have a name."),
)
if reservation.partner_id.lastname:
firstname = reservation.partner_id.lastname
elif reservation.partner_name and len(reservation.partner_name.split(" ")) > 1:
firstname = reservation.partner_name.split(" ")[1]
else:
firstname = "No aplica"
ET.SubElement(persona, "apellido1").text = firstname
elif checkin_partner:
_ses_xml_text_element_and_validate(
persona,
"nombre",
checkin_partner.firstname,
_("The guest does not have a name."),
)
_ses_xml_text_element_and_validate(
persona,
"apellido1",
checkin_partner.lastname,
_("The guest does not have a lastname."),
)
if checkin_partner.document_type.code == CODE_DNI:
_ses_xml_text_element_and_validate(
persona,
"apellido2",
checkin_partner.partner_id.lastname2,
_("The guest does not have a second lastname."),
)
def _ses_xml_person_personal_info_elements(persona, checkin_partner):
ET.SubElement(persona, "rol").text = "VI"
_ses_xml_person_names_elements(
persona, reservation=False, checkin_partner=checkin_partner
)
if checkin_partner.document_type.code:
document_type = _ses_xml_map_document_type(checkin_partner.document_type.code)
ET.SubElement(persona, "tipoDocumento").text = document_type
else:
raise ValidationError(_("The guest does not have a document type."))
_ses_xml_text_element_and_validate(
persona,
"numeroDocumento",
checkin_partner.document_number,
_("The guest does not have a document number."),
)
if checkin_partner.document_type.code in [CODE_DNI, CODE_NIE]:
_ses_xml_text_element_and_validate(
persona,
"soporteDocumento",
checkin_partner.support_number,
_("The guest does not have a support number."),
)
_ses_xml_text_element_and_validate(
persona,
"fechaNacimiento",
str(checkin_partner.birthdate_date)[:10],
_("The guest does not have a birthdate."),
)
def _ses_xml_municipality_code(residence_zip):
with open(
get_module_resource(
"pms_l10n_es", "static/src/", "pms.ine.zip.municipality.ine.relation.csv"
),
"r",
newline="",
) as f:
lector = csv.reader(f)
for fila in lector:
if residence_zip in fila[0]:
return fila[1][:5]
raise ValidationError(_("The guest does not have a valid zip code."))
def _ses_xml_person_address_elements(persona, checkin_partner):
direccion = ET.SubElement(persona, "direccion")
_ses_xml_text_element_and_validate(
direccion,
"direccion",
checkin_partner.residence_street,
_("The guest does not have a street."),
)
if checkin_partner.residence_country_id.code == CODE_SPAIN:
municipio_code = _ses_xml_municipality_code(checkin_partner.residence_zip)
if municipio_code:
ET.SubElement(direccion, "codigoMunicipio").text = municipio_code
else:
_ses_xml_text_element_and_validate(
direccion,
"nombreMunicipio",
checkin_partner.residence_city,
_("The guest does not have a city."),
)
_ses_xml_text_element_and_validate(
direccion,
"codigoPostal",
checkin_partner.residence_zip,
_("The guest does not have a zip code."),
)
_ses_xml_text_element_and_validate(
direccion,
"pais",
checkin_partner.residence_country_id.code_alpha3,
_("The guest does not have a country."),
)
def _ses_xml_person_contact_elements(persona, reservation, checkin_partner=False):
partner = reservation.partner_id
contact_methods = []
if checkin_partner:
contact_methods.extend(
[
checkin_partner.mobile,
checkin_partner.phone,
checkin_partner.email,
]
)
contact_methods.extend(
[
partner.mobile,
partner.phone,
partner.email,
reservation.email,
reservation.pms_property_id.partner_id.email,
reservation.pms_property_id.partner_id.phone,
]
)
for contact in contact_methods:
if contact:
tag = "telefono" if "@" not in contact else "correo"
ET.SubElement(persona, tag).text = contact
break
else:
raise ValidationError(
_(
"The guest/reservation partner and property does not "
"have a contact method (mail or phone)"
)
)
def _ses_xml_person_elements(comunicacion, checkin_partner):
persona = ET.SubElement(comunicacion, "persona")
_ses_xml_person_personal_info_elements(persona, checkin_partner)
_ses_xml_person_address_elements(persona, checkin_partner)
_ses_xml_person_contact_elements(
persona, checkin_partner.reservation_id, checkin_partner
)
def _get_auth_headers(communication):
user = communication.reservation_id.pms_property_id.institution_user
password = communication.reservation_id.pms_property_id.institution_password
user_and_password_base64 = "Basic " + base64.b64encode(
bytes(user + ":" + password, "utf-8")
).decode("utf-8")
return {
"Authorization": user_and_password_base64,
"Content-Type": "text/xml; charset=utf-8",
}
def _generate_payload(lessor_id, operation, entity, data):
return f"""
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:com="http://www.soap.servicios.hospedajes.mir.es/comunicacion">
<soapenv:Header/>
<soapenv:Body>
<com:comunicacionRequest>
<peticion>
<cabecera>
<codigoArrendador>{lessor_id}</codigoArrendador>
<aplicacion>Roomdoo</aplicacion>
<tipoOperacion>{operation}</tipoOperacion>
<tipoComunicacion>{entity}</tipoComunicacion>
</cabecera>
<solicitud>{data}</solicitud>
</peticion>
</com:comunicacionRequest>
</soapenv:Body>
</soapenv:Envelope>
"""
def _handle_request_exception(communication, e):
if isinstance(e, requests.exceptions.RequestException):
if isinstance(e, requests.exceptions.ConnectionError):
if communication.state == "to_send":
communication.sending_result = "Cannot establish the connection."
else:
communication.processing_result = "Cannot establish the connection."
elif isinstance(e, requests.exceptions.Timeout):
if communication.state == "to_send":
communication.sending_result = "The request took too long to complete."
else:
communication.processing_result = (
"The request took too long to complete."
)
else:
if communication.state == "to_send":
communication.sending_result = f"Request error: {e}"
else:
communication.processing_result = f"Request error: {e}"
class TravellerReport(models.TransientModel):
_name = "traveller.report.wizard"
@@ -22,10 +326,26 @@ class TravellerReport(models.TransientModel):
txt_filename = fields.Text()
txt_binary = fields.Binary(string="File Download")
txt_message = fields.Char(string="File Preview")
txt_message = fields.Char(
string="File Preview",
readonly=True,
store=True,
compute="_compute_txt_message",
)
date_target = fields.Date(
string="Date", required=True, default=lambda self: fields.Date.today()
)
date_from = fields.Date(
string="From",
required=True,
default=lambda self: fields.Date.today(),
)
date_to = fields.Date(
string="To",
required=True,
default=lambda self: fields.Date.today() + relativedelta(days=1),
)
pms_property_id = fields.Many2one(
comodel_name="pms.property",
string="Property",
@@ -33,6 +353,35 @@ class TravellerReport(models.TransientModel):
default=lambda self: self.env.user.get_active_property_ids()[0],
)
is_ses = fields.Boolean(
string="Is SES",
readonly=True,
compute="_compute_is_ses",
)
report_type = fields.Selection(
string="Report Type",
required=True,
default="reservations",
help="Report type (reservation/traveller report)",
selection=[
("reservations", "Reservations Report"),
("travellers", "Travellers Report"),
],
)
@api.depends(
"pms_property_id", "date_target", "date_from", "date_to", "report_type"
)
def _compute_txt_message(self):
for record in self:
record.txt_message = False
@api.depends("pms_property_id.institution")
def _compute_is_ses(self):
for record in self:
record.is_ses = record.pms_property_id.institution == "ses"
def generate_file_from_user_action(self):
pms_property = self.env["pms.property"].search(
[("id", "=", self.pms_property_id.id)]
@@ -45,22 +394,64 @@ class TravellerReport(models.TransientModel):
or not pms_property.institution_password
):
raise ValidationError(
_("The guest information sending settings is not property updated.")
_("The guest information sending settings is not property set up.")
)
content = False
# build content
content = self.generate_checkin_list(
pms_property_id=pms_property.id,
date_target=self.date_target,
)
if self.is_ses:
if self.report_type == "travellers":
content = self.generate_ses_travellers_list(
pms_property_id=pms_property.id,
date_target=self.date_target,
)
elif self.report_type == "reservations":
content = self.generate_ses_reservation_list(
pms_property_id=pms_property.id,
date_from=self.date_from,
date_to=self.date_to,
)
else:
content = self.generate_checkin_list(
pms_property_id=pms_property.id,
date_target=self.date_target,
)
if content:
self.txt_filename = pms_property.institution_property_id + ".999"
if self.is_ses:
if self.report_type == "travellers":
self.txt_filename = (
pms_property.institution_property_id
+ "-"
+ self.date_target.strftime("%Y%m%d")
)
else:
self.txt_filename = (
pms_property.institution_property_id
+ "-"
+ self.date_from.strftime("%Y%m%d")
+ "-"
+ self.date_to.strftime("%Y%m%d")
)
self.txt_filename = self.txt_filename + ".xml"
else:
self.txt_filename = (
pms_property.institution_property_id
+ "-"
+ self.date_target.strftime("%Y%m%d")
+ ".999"
)
self.txt_binary = base64.b64encode(str.encode(content))
self.txt_message = content
return {
"name": _("Traveller Report"),
"name": _(
"Travellers Report"
if self.report_type == "travellers" or not self.is_ses
else "Reservations Report"
),
"res_id": self.id,
"res_model": "traveller.report.wizard",
"target": "new",
@@ -450,3 +841,290 @@ class TravellerReport(models.TransientModel):
if prop.institution:
self.send_file_institution(pms_property=prop, offset=offset)
time.sleep(0.5)
# SES RESERVATIONS
def generate_ses_reservation_list(self, pms_property_id, date_from, date_to):
reservation_ids = (
self.env["pms.reservation"]
.search(
[
("pms_property_id", "=", pms_property_id),
("state", "!=", "cancel"),
("reservation_type", "!=", "out"),
"|",
("date_order", ">=", date_from),
("date_order", "<=", date_to),
]
)
.mapped("id")
)
return self.generate_xml_reservations(reservation_ids)
def generate_xml_reservation(self, solicitud, reservation_id):
reservation = self.env["pms.reservation"].browse(reservation_id)
if not reservation.pms_property_id.institution_property_id:
raise ValidationError(
_("The property does not have an institution property id.")
)
# SOLICITUD > COMUNICACION
comunicacion = ET.SubElement(solicitud, "comunicacion")
# SOLICITUD > COMUNICACION > ESTABLECIMIENTO
establecimiento = ET.SubElement(comunicacion, "establecimiento")
# SOLICITUD > COMUNICACION > ESTABLECIMIENTO > CODIGO
ET.SubElement(
establecimiento, "codigo"
).text = reservation.pms_property_id.institution_property_id
# SOLICITUD > COMUNICACION > CONTRATO
_ses_xml_contract_elements(comunicacion, reservation)
# SOLICITUD > COMUNICACION > PERSONA
persona = ET.SubElement(comunicacion, "persona")
# SOLICITUD > COMUNICACION > PERSONA > ROL
ET.SubElement(persona, "rol").text = "TI"
# SOLICITUD > COMUNICACION > PERSONA > NOMBRE
_ses_xml_person_names_elements(persona, reservation, checkin_partner=None)
_ses_xml_person_contact_elements(persona, reservation)
def generate_xml_reservations(self, reservation_ids):
if not reservation_ids:
raise ValidationError(_("Theres's no reservation to generate the XML"))
# SOLICITUD
solicitud = ET.Element("solicitud")
for reservation_id in reservation_ids:
ET.SubElement(
solicitud,
self.generate_xml_reservation(solicitud, reservation_id),
)
xml_str = ET.tostring(solicitud, encoding="unicode")
xml_str = (
'<ns2:peticion xmlns:ns2="http://www.neg.hospedajes.mir.es/altaReservaHospedaje">'
+ xml_str
+ "</ns2:peticion>"
)
return xml_str
# SES RESERVATIONS TRAVELLER REPORT
def generate_ses_travellers_list(self, pms_property_id, date_target):
reservation_ids = (
self.env["pms.reservation"]
.search(
[
("pms_property_id", "=", pms_property_id),
("checkin", "=", date_target),
]
)
.mapped("id")
)
return self.generate_xml_reservations_travellers_report(reservation_ids)
def generate_xml_reservation_travellers_report(self, solicitud, reservation_id):
reservation = self.env["pms.reservation"].browse(reservation_id)
comunicacion = ET.SubElement(solicitud, "comunicacion")
_ses_xml_contract_elements(comunicacion, reservation)
for checkin_partner in reservation.checkin_partner_ids.filtered(
lambda x: x.state == "onboard"
):
_ses_xml_person_elements(comunicacion, checkin_partner)
def generate_xml_reservations_travellers_report(self, reservation_ids):
if not reservation_ids:
raise ValidationError(_("Theres's no reservation to generate the XML"))
if (
len(
self.env["pms.reservation"]
.browse(reservation_ids)
.mapped("pms_property_id")
)
> 1
):
raise ValidationError(_("The reservations must be from the same property."))
if not any(
state == "onboard"
for state in self.env["pms.reservation"]
.browse(reservation_ids)
.mapped("checkin_partner_ids")
.mapped("state")
):
raise ValidationError(
_("There are no guests to generate the travellers report.")
)
# SOLICITUD
solicitud = ET.Element("solicitud")
pms_property = (
self.env["pms.reservation"].browse(reservation_ids[0]).pms_property_id
)
if not pms_property.institution_property_id:
raise ValidationError(
_("The property does not have an institution property id.")
)
# SOLICITUD -> CODIGO ESTABLECIMIENTO
ET.SubElement(
solicitud, "codigoEstablecimiento"
).text = pms_property.institution_property_id
for reservation_id in reservation_ids:
ET.SubElement(
solicitud,
self.generate_xml_reservation_travellers_report(
solicitud, reservation_id
),
)
xml_str = ET.tostring(solicitud, encoding="unicode")
xml_str = (
'<ns2:peticion xmlns:ns2="http://www.neg.hospedajes.mir.es/altaParteHospedaje">'
+ xml_str
+ "</ns2:peticion>"
)
return xml_str
@api.model
def ses_send_communications(self, entity):
for communication in self.env["pms.ses.communication"].search(
[
("state", "=", "to_send"),
("entity", "=", entity),
]
):
data = False
if communication.entity == "RH":
data = self.generate_xml_reservations([communication.reservation_id.id])
elif communication.entity == "PV":
data = self.generate_xml_reservations_travellers_report(
[communication.reservation_id.id]
)
communication.communication_xml = data
data = _string_to_zip_to_base64(data)
payload = _generate_payload(
communication.reservation_id.pms_property_id.institution_lessor_id,
communication.operation,
communication.entity,
data,
)
communication.communication_soap = payload
communication.communication_time = fields.Datetime.now()
try:
soap_response = requests.request(
"POST",
communication.reservation_id.pms_property_id.ses_url,
headers=_get_auth_headers(communication),
data=payload,
verify=False,
)
root = ET.fromstring(soap_response.text)
communication.sending_result = root.find(".//descripcion").text
communication.response_communication_soap = soap_response.text
result_code = root.find(".//codigo").text
if result_code == REQUEST_CODE_OK:
communication.communication_id = root.find(".//lote").text
if communication.operation == "A":
communication.state = "to_process"
else:
communication.state = "processed"
else:
communication.state = "error_sending"
except requests.exceptions.RequestException as e:
_handle_request_exception(communication, e)
@api.model
def ses_process_communications(self):
for communication in self.env["pms.ses.communication"].search(
[
("state", "=", "to_process"),
("operation", "!=", DELETE_OPERATION_CODE),
]
):
var_xml_get_batch = f"""
<con:lotes
xmlns:con="http://www.neg.hospedajes.mir.es/consultarComunicacion">
<con:lote>{communication.communication_id}</con:lote>
</con:lotes>
"""
communication.query_status_xml = var_xml_get_batch
data = _string_to_zip_to_base64(var_xml_get_batch)
payload = _generate_payload(
communication.reservation_id.pms_property_id.institution_lessor_id,
"C",
"",
data,
)
communication.query_status_soap = payload
communication.query_status_time = fields.Datetime.now()
try:
soap_response = requests.request(
"POST",
communication.reservation_id.pms_property_id.ses_url,
headers=_get_auth_headers(communication),
data=payload,
verify=False,
)
root = ET.fromstring(soap_response.text)
communication.response_communication_soap = soap_response.text
result_code = root.find(".//codigo").text
communication.response_query_status_soap = soap_response.text
if result_code == REQUEST_CODE_OK:
result_status = root.find(".//codigoEstado").text
if result_status == XML_OK:
communication.state = "processed"
communication.processing_result = root.find(
".//descripcion"
).text
elif result_status in [XML_PROCESSING, XML_PENDING]:
communication.state = "to_process"
communication.processing_result = "Not processed yet"
else:
communication.state = "error_processing"
communication.processing_result = root.find(".//error").text
# request errors
else:
communication.state = "error_processing"
communication.processing_result = root.find(".//descripcion").text
except requests.exceptions.RequestException as e:
_handle_request_exception(communication, e)
@api.model
def create_pending_notifications_traveller_report(self):
domain = [
("state", "=", "onboard"),
("checkin", "=", fields.Datetime.today().date()),
("pms_property_id.institution", "=", "ses"),
]
for reservation in (
self.env["pms.reservation"]
.search(domain)
.filtered(
lambda x: any(
state == "onboard"
for state in x.checkin_partner_ids.mapped("state")
)
)
):
self.env["pms.ses.communication"].search(
[
("reservation_id", "=", reservation.id),
("entity", "=", "PV"),
("state", "=", "to_send"),
]
).unlink()
self.env["pms.reservation"].create_communication(
reservation.id,
"A",
"PV",
)

View File

@@ -9,10 +9,40 @@
<div class="row">
<div class="col-12">
<group>
<field name="is_ses" invisible="1" />
<field name="pms_property_id" />
<field name="date_target" />
<field
name="report_type"
attrs="{'invisible': [('is_ses', '=', False)]}"
/>
<field
name="date_target"
attrs="{'invisible': [('is_ses', '=', True), ('report_type', '=', 'reservations')]}"
/>
<field
name="date_from"
attrs="{'invisible': [
'|',
'&amp;',
('is_ses', '=', True),
('report_type', '=', 'travellers'),
('is_ses', '=', False),
]}"
/>
<field
name="date_to"
attrs="{'invisible': [
'|',
'&amp;',
('is_ses', '=', True),
('report_type', '=', 'travellers'),
('is_ses', '=', False),
]}"
/>
</group>
<group attrs="{'invisible': [('txt_message','=',False)]}">
<group
attrs="{'invisible': ['|', ('txt_message','=',False), ('is_ses', '=', True)]}"
>
<field name="txt_message" readonly="1" />
</group>
<group attrs="{'invisible': [('txt_message','=',False)]}">
@@ -25,26 +55,30 @@
</div>
</div>
<div class="row ">
<div class="col-3">
<button
<div class="row ">
<div class="col-3">
<button
name="generate_file_from_user_action"
class="btn btn-primary btn-sm"
type="object"
string="Preview file"
string="Generate file"
/>
</div>
<div class="col-3">
<button
</div>
<div class="col-3">
<button
name="send_file_institution"
class="btn btn-primary btn-sm"
type="object"
string="Send file"
attrs="{'invisible': [('txt_message','=',False)]}"
attrs="{'invisible': [
'|',
('txt_message','=',False),
('is_ses', '=', True),
]}"
/>
</div>
</div>
<footer />
</div>
</form>
</field>
</record>

View File

@@ -238,7 +238,7 @@ class WizardIne(models.TransientModel):
"""
for entry in read_group_result:
if not entry["nationality_id"]:
if not entry["residence_country_id"]:
guests_with_no_nationality = self.env["pms.checkin.partner"].search(
entry["__domain"]
)
@@ -253,24 +253,24 @@ class WizardIne(models.TransientModel):
guests_with_no_nationality,
)
)
# get nationality_id from group set read_group results
nationality_id_code = (
# get residence_country_id from group set read_group results
residence_country_id_code = (
self.env["res.country"]
.search([("id", "=", entry["nationality_id"][0])])
.search([("id", "=", entry["residence_country_id"][0])])
.code
)
# all countries except Spain
if nationality_id_code != CODE_SPAIN:
if residence_country_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
if not nationalities.get(residence_country_id_code):
nationalities[residence_country_id_code] = dict()
if not nationalities[residence_country_id_code].get(date):
nationalities[residence_country_id_code][date] = dict()
nationalities[residence_country_id_code][date][type_of_entry] = num
else:
# arrivals grouped by state_id (Spain "provincias")
read_by_arrivals_spain = self.env["pms.checkin.partner"].read_group(
@@ -351,36 +351,36 @@ class WizardIne(models.TransientModel):
# arrivals
arrivals = hosts.filtered(lambda x: x.reservation_id.checkin == p_date)
# arrivals grouped by nationality_id
# arrivals grouped by residence_country_id
read_by_arrivals = self.env["pms.checkin.partner"].read_group(
[("id", "in", arrivals.ids)],
["nationality_id"],
["nationality_id"],
orderby="nationality_id",
["residence_country_id"],
["residence_country_id"],
orderby="residence_country_id",
lazy=False,
)
# departures
departures = hosts.filtered(lambda x: x.reservation_id.checkout == p_date)
# departures grouped by nationality_id
# departures grouped by residence_country_id
read_by_departures = self.env["pms.checkin.partner"].read_group(
[("id", "in", departures.ids)],
["nationality_id"],
["nationality_id"],
orderby="nationality_id",
["residence_country_id"],
["residence_country_id"],
orderby="residence_country_id",
lazy=False,
)
# pernoctations
pernoctations = hosts - departures
# pernoctations grouped by nationality_id
# pernoctations grouped by residence_country_id
read_by_pernoctations = self.env["pms.checkin.partner"].read_group(
[("id", "in", pernoctations.ids)],
["nationality_id"],
["nationality_id"],
orderby="nationality_id",
["residence_country_id"],
["residence_country_id"],
orderby="residence_country_id",
lazy=False,
)
ine_add_arrivals_departures_pernoctations(