diff --git a/pms_l10n_es/data/cron_jobs.xml b/pms_l10n_es/data/cron_jobs.xml
index da8e85637..9028125eb 100644
--- a/pms_l10n_es/data/cron_jobs.xml
+++ b/pms_l10n_es/data/cron_jobs.xml
@@ -19,24 +19,24 @@
model.send_file_institution_async()
-
+
- SES Automatic Creation Traveller Communications
+ SES Automatic Sending Incomplete Traveller Reports
1
- days
+ hours
-1
code
- model.create_pending_notifications_traveller_report()
+ model.ses_send_incomplete_traveller_reports(20)
diff --git a/pms_l10n_es/models/pms_checkin_partner.py b/pms_l10n_es/models/pms_checkin_partner.py
index ca3ca9f0b..8eb72d30d 100644
--- a/pms_l10n_es/models/pms_checkin_partner.py
+++ b/pms_l10n_es/models/pms_checkin_partner.py
@@ -2,6 +2,8 @@ import logging
from odoo import api, fields, models
+from ..wizards.traveller_report import CREATE_OPERATION_CODE
+
CODE_SPAIN = "ES"
CODE_NIF = "D"
CODE_NIE = "N"
@@ -80,3 +82,47 @@ class PmsCheckinPartner(models.Model):
manual_fields = super(PmsCheckinPartner, self)._checkin_manual_fields()
manual_fields.extend(["support_number"])
return manual_fields
+
+ def write(self, vals):
+ result = super(PmsCheckinPartner, self).write(vals)
+ for record in self:
+ if (
+ "state" in vals
+ and record.reservation_id.pms_property_id.institution == "ses"
+ and record.state == "onboard"
+ ):
+ previous_incomplete_traveller_communication = self.env[
+ "pms.ses.communication"
+ ].search(
+ [
+ ("reservation_id", "=", record.reservation_id),
+ ("entity", "=", "PV"),
+ ("operation", "=", CREATE_OPERATION_CODE),
+ ("state", "=", "incomplete"),
+ ]
+ )
+ if not previous_incomplete_traveller_communication:
+ previous_incomplete_traveller_communication = self.env[
+ "pms.ses.communication"
+ ].create(
+ {
+ "reservation_id": record.reservation_id,
+ "operation": CREATE_OPERATION_CODE,
+ "entity": "PV",
+ "state": "incomplete",
+ }
+ )
+ # check if all checkin partners in the reservation are onboard
+ if (
+ all(
+ [
+ partner.state == "onboard"
+ for partner in record.reservation_id.checkin_partner_ids
+ ]
+ )
+ and len(record.reservation_id.checkin_partner_ids)
+ == record.reservation_id.adults
+ ):
+ previous_incomplete_traveller_communication.state = "to_send"
+
+ return result
diff --git a/pms_l10n_es/models/pms_reservation.py b/pms_l10n_es/models/pms_reservation.py
index ed4042ce6..9a0877f34 100644
--- a/pms_l10n_es/models/pms_reservation.py
+++ b/pms_l10n_es/models/pms_reservation.py
@@ -101,4 +101,3 @@ class PmsReservation(models.Model):
if record.pms_property_id.institution == "ses":
self.create_communication_after_update_reservation(record, vals)
return super(PmsReservation, self).write(vals)
-
diff --git a/pms_l10n_es/wizards/traveller_report.py b/pms_l10n_es/wizards/traveller_report.py
index 3d5ab0881..16dbfd93a 100644
--- a/pms_l10n_es/wizards/traveller_report.py
+++ b/pms_l10n_es/wizards/traveller_report.py
@@ -32,10 +32,19 @@ XML_PENDING = "5"
CREATE_OPERATION_CODE = "A"
DELETE_OPERATION_CODE = "B"
+
# Disable insecure request warnings
# requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
+def clean_string_ses(string):
+ clean_string = re.sub(r"[^a-zA-Z0-9\s]", "", string).upper()
+
+ clean_string = " ".join(clean_string.split())
+
+ return clean_string
+
+
def _string_to_zip_to_base64(string_data):
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
@@ -56,7 +65,7 @@ def _ses_xml_payment_elements(contrato, reservation):
ET.SubElement(pago, "tipoPago").text = tipo_pago
-def _ses_xml_contract_elements(comunicacion, reservation):
+def _ses_xml_contract_elements(comunicacion, reservation, people=False):
contrato = ET.SubElement(comunicacion, "contrato")
ET.SubElement(contrato, "referencia").text = reservation.name
ET.SubElement(contrato, "fechaContrato").text = str(reservation.date_order)[:10]
@@ -66,7 +75,10 @@ def _ses_xml_contract_elements(comunicacion, reservation):
ET.SubElement(
contrato, "fechaSalida"
).text = f"{str(reservation.checkout)[:10]}T00:00:00"
- ET.SubElement(contrato, "numPersonas").text = str(reservation.adults)
+ if people:
+ ET.SubElement(contrato, "numPersonas").text = str(people)
+ else:
+ ET.SubElement(contrato, "numPersonas").text = str(reservation.adults)
_ses_xml_payment_elements(contrato, reservation)
@@ -92,9 +104,9 @@ def _ses_xml_person_names_elements(persona, reservation, checkin_partner):
if reservation:
ses_firstname = False
if reservation.partner_id.firstname:
- ses_firstname = reservation.partner_id.firstname
+ ses_firstname = clean_string_ses(reservation.partner_id.firstname)
elif reservation.partner_name:
- ses_firstname = reservation.partner_name.split(" ")[0]
+ ses_firstname = clean_string_ses(reservation.partner_name).split(" ")[0]
_ses_xml_text_element_and_validate(
persona,
"nombre",
@@ -103,9 +115,9 @@ def _ses_xml_person_names_elements(persona, reservation, checkin_partner):
)
if reservation.partner_id.lastname:
- ses_lastname = reservation.partner_id.lastname
+ ses_lastname = clean_string_ses(reservation.partner_id.lastname)
elif reservation.partner_name and len(reservation.partner_name.split(" ")) > 1:
- ses_lastname = reservation.partner_name.replace(" ", " ").split(" ")[1]
+ ses_lastname = clean_string_ses(reservation.partner_name).split(" ")[1]
else:
ses_lastname = "No aplica"
ET.SubElement(persona, "apellido1").text = ses_lastname
@@ -114,13 +126,13 @@ def _ses_xml_person_names_elements(persona, reservation, checkin_partner):
_ses_xml_text_element_and_validate(
persona,
"nombre",
- checkin_partner.firstname,
+ clean_string_ses(checkin_partner.firstname),
_("The guest does not have a name."),
)
_ses_xml_text_element_and_validate(
persona,
"apellido1",
- checkin_partner.lastname,
+ clean_string_ses(checkin_partner.lastname),
_("The guest does not have a lastname."),
)
@@ -128,7 +140,7 @@ def _ses_xml_person_names_elements(persona, reservation, checkin_partner):
_ses_xml_text_element_and_validate(
persona,
"apellido2",
- checkin_partner.partner_id.lastname2,
+ clean_string_ses(checkin_partner.partner_id.lastname2),
_("The guest does not have a second lastname."),
)
@@ -758,8 +770,9 @@ class TravellerReport(models.TransientModel):
)
def send_file_institution(self, pms_property=False, offset=0, date_target=False):
+ called_from_user = False
+ log = False
try:
- called_from_user = False
if not pms_property:
called_from_user = True
pms_property = self.env["pms.property"].search(
@@ -939,21 +952,23 @@ class TravellerReport(models.TransientModel):
)
return self.generate_xml_reservations_travellers_report(reservation_ids)
- def generate_xml_reservation_travellers_report(self, solicitud, reservation_id):
+ def generate_xml_reservation_travellers_report(
+ self, solicitud, reservation_id, people=False
+ ):
reservation = self.env["pms.reservation"].browse(reservation_id)
comunicacion = ET.SubElement(solicitud, "comunicacion")
- _ses_xml_contract_elements(comunicacion, reservation)
-
+ _ses_xml_contract_elements(comunicacion, reservation, people)
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):
+ def generate_xml_reservations_travellers_report(
+ self, reservation_ids, ignore_some_not_onboard=False
+ ):
if not reservation_ids:
raise ValidationError(_("Theres's no reservation to generate the XML"))
-
- if (
+ elif (
len(
self.env["pms.reservation"]
.browse(reservation_ids)
@@ -962,49 +977,64 @@ class TravellerReport(models.TransientModel):
> 1
):
raise ValidationError(_("The reservations must be from the same property."))
- if not any(
- state == "onboard"
+ elif all(
+ 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.")
+ raise ValidationError(_("There are no guests onboard."))
+ elif not ignore_some_not_onboard and any(
+ state != "onboard"
+ for state in self.env["pms.reservation"]
+ .browse(reservation_ids)
+ .mapped("checkin_partner_ids")
+ .mapped("state")
+ ):
+ raise ValidationError(_("There are some guests not onboard."))
+ else:
+ # SOLICITUD
+ solicitud = ET.Element("solicitud")
+ pms_property = (
+ self.env["pms.reservation"].browse(reservation_ids[0]).pms_property_id
)
-
- # 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:
+ if not pms_property.institution_property_id:
+ raise ValidationError(
+ _("The property does not have an institution property id.")
+ )
+ # SOLICITUD -> CODIGO ESTABLECIMIENTO
ET.SubElement(
- solicitud,
- self.generate_xml_reservation_travellers_report(
- solicitud, reservation_id
- ),
+ solicitud, "codigoEstablecimiento"
+ ).text = pms_property.institution_property_id
+ for reservation_id in reservation_ids:
+ if ignore_some_not_onboard:
+ num_people_on_board = len(
+ self.env["pms.reservation"]
+ .browse(reservation_id)
+ .checkin_partner_ids.filtered(lambda x: x.state == "onboard")
+ )
+ ET.SubElement(
+ solicitud,
+ self.generate_xml_reservation_travellers_report(
+ solicitud, reservation_id, people=num_people_on_board
+ ),
+ )
+ else:
+ ET.SubElement(
+ solicitud,
+ self.generate_xml_reservation_travellers_report(
+ solicitud,
+ reservation_id,
+ ),
+ )
+ xml_str = ET.tostring(solicitud, encoding="unicode")
+ xml_str = (
+ ''
+ + xml_str
+ + ""
)
- xml_str = ET.tostring(solicitud, encoding="unicode")
-
- xml_str = (
- ''
- + xml_str
- + ""
- )
- return xml_str
+ return xml_str
@api.model
def ses_send_communications(self, entity):
@@ -1015,32 +1045,34 @@ class TravellerReport(models.TransientModel):
("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:
- 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]
+ if communication.operation == DELETE_OPERATION_CODE:
+ communication_to_cancel = self.env["pms.ses.communication"].search(
+ [
+ ("reservation_id", "=", communication.reservation_id.id),
+ ("state", "!=", "to_send"),
+ ("entity", "=", communication.entity),
+ ("operation", "=", CREATE_OPERATION_CODE),
+ ]
)
+ data = (
+ "'
+ + ""
+ + communication_to_cancel.communication_id
+ + ""
+ + ""
+ )
+ elif communication.operation == CREATE_OPERATION_CODE:
+ 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(
@@ -1057,7 +1089,7 @@ class TravellerReport(models.TransientModel):
communication.reservation_id.pms_property_id.ses_url,
headers=_get_auth_headers(communication),
data=payload,
- verify=get_module_resource("pms_l10n_es", "static", "ses_cert.pem"),
+ verify=get_module_resource("pms_l10n_es", "static", "cert.pem"),
)
root = ET.fromstring(soap_response.text)
communication.sending_result = root.find(".//descripcion").text
@@ -1077,6 +1109,70 @@ class TravellerReport(models.TransientModel):
except Exception as e:
_handle_request_exception(communication, e)
+ @api.model
+ def ses_send_incomplete_traveller_reports(
+ self, hours_after_first_checkin_to_inform
+ ):
+ # iterate through incomplete communications
+ for communication in self.env["pms.ses.communication"].search(
+ [
+ ("state", "=", "incomplete"),
+ ("entity", "=", "PV"),
+ ]
+ ):
+ try:
+ if (
+ fields.Datetime.now() - communication.create_date
+ ).hours > hours_after_first_checkin_to_inform:
+ # add a note to the reservation
+ communication.reservation_id.sudo().message_post(
+ body=_(
+ "There was't enough guests in the reservation when data "
+ "was sent to SES. Sent to SES with onboard guests"
+ )
+ )
+ data = self.generate_xml_reservations_travellers_report(
+ [communication.reservation_id.id],
+ ignore_some_not_onboard=True,
+ )
+ 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()
+
+ soap_response = requests.request(
+ "POST",
+ communication.reservation_id.pms_property_id.ses_url,
+ headers=_get_auth_headers(communication),
+ data=payload,
+ verify=get_module_resource(
+ "pms_l10n_es", "static", "ses_cert.pem"
+ ),
+ )
+ 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 == CREATE_OPERATION_CODE:
+ communication.state = "to_process"
+ else:
+ communication.state = "processed"
+ else:
+ communication.state = "error_sending"
+
+ except requests.exceptions.RequestException as e:
+ _handle_request_exception(communication, e)
+ except Exception as e:
+ _handle_request_exception(communication, e)
+
@api.model
def ses_process_communications(self):
for communication in self.env["pms.ses.communication"].search(
@@ -1085,22 +1181,6 @@ class TravellerReport(models.TransientModel):
("operation", "!=", DELETE_OPERATION_CODE),
]
):
- var_xml_get_batch = f"""
-
- {communication.communication_id}
-
- """
- 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:
var_xml_get_batch = f"""