diff --git a/pms_l10n_es/data/cron_jobs.xml b/pms_l10n_es/data/cron_jobs.xml index da8e85637..f8e0d7934 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..e72ab0125 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.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.id, + "operation": CREATE_OPERATION_CODE, + "entity": "PV", + "state": "incomplete", + } + ) + # check if all checkin partners in the reservation are onboard + if ( + all( + [ + checkin.state == "onboard" + for checkin 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 2ec6c3549..f1a425211 100644 --- a/pms_l10n_es/models/pms_reservation.py +++ b/pms_l10n_es/models/pms_reservation.py @@ -81,9 +81,9 @@ class PmsReservation(models.Model): self.create_communication( reservation.id, DELETE_OPERATION_CODE, "RH" ) - elif ( - vals["state"] != "cancel" - and last_communication.operation == DELETE_OPERATION_CODE + elif vals["state"] != "cancel" and ( + last_communication.operation == DELETE_OPERATION_CODE + or not last_communication ): self.create_communication( reservation.id, CREATE_OPERATION_CODE, "RH" diff --git a/pms_l10n_es/models/pms_ses_communication.py b/pms_l10n_es/models/pms_ses_communication.py index 5c3d6ef0e..6119cea67 100644 --- a/pms_l10n_es/models/pms_ses_communication.py +++ b/pms_l10n_es/models/pms_ses_communication.py @@ -44,6 +44,7 @@ class PmsSesCommunication(models.Model): default="to_send", required=True, selection=[ + ("incomplete", "Incomplete checkin data"), ("to_send", "Pending Notification"), ("to_process", "Pending Processing"), ("error_sending", "Error Sending"), @@ -51,6 +52,7 @@ class PmsSesCommunication(models.Model): ("processed", "Processed"), ], ) + sending_result = fields.Text( string="Sending Result", help="Notification sending result", diff --git a/pms_l10n_es/wizards/traveller_report.py b/pms_l10n_es/wizards/traveller_report.py index 94ecd91a8..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) @@ -90,37 +102,37 @@ def _ses_xml_map_document_type(code): def _ses_xml_person_names_elements(persona, reservation, checkin_partner): if reservation: - name = False + ses_firstname = False if reservation.partner_id.firstname: - name = reservation.partner_id.firstname + ses_firstname = clean_string_ses(reservation.partner_id.firstname) elif reservation.partner_name: - name = reservation.partner_name.split(" ")[0] + ses_firstname = clean_string_ses(reservation.partner_name).split(" ")[0] _ses_xml_text_element_and_validate( persona, "nombre", - name, + ses_firstname, _("The reservation does not have a name."), ) if reservation.partner_id.lastname: - firstname = reservation.partner_id.lastname + ses_lastname = clean_string_ses(reservation.partner_id.lastname) elif reservation.partner_name and len(reservation.partner_name.split(" ")) > 1: - firstname = reservation.partner_name.split(" ")[1] + ses_lastname = clean_string_ses(reservation.partner_name).split(" ")[1] else: - firstname = "No aplica" - ET.SubElement(persona, "apellido1").text = firstname + ses_lastname = "No aplica" + ET.SubElement(persona, "apellido1").text = ses_lastname elif 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."), ) @@ -242,7 +254,12 @@ def _ses_xml_person_contact_elements(persona, reservation, checkin_partner=False for contact in contact_methods: if contact: - tag = "telefono" if "@" not in contact else "correo" + if "@" in contact: + tag = "correo" + contact = contact[0:50] + else: + tag = "telefono" + contact = contact[0:20] ET.SubElement(persona, tag).text = contact break else: @@ -324,6 +341,8 @@ def _handle_request_exception(communication, e): communication.sending_result = f"Request error: {e}" else: communication.processing_result = f"Request error: {e}" + else: + communication.sending_result = f"Unexpected error: {e}" class TravellerReport(models.TransientModel): @@ -751,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( @@ -932,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) @@ -955,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): @@ -1008,31 +1045,51 @@ 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: + 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( + 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"), + verify=get_module_resource("pms_l10n_es", "static", "cert.pem"), ) root = ET.fromstring(soap_response.text) communication.sending_result = root.find(".//descripcion").text @@ -1049,6 +1106,72 @@ class TravellerReport(models.TransientModel): except requests.exceptions.RequestException as e: _handle_request_exception(communication, e) + 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): @@ -1058,23 +1181,24 @@ 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""" + + {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() + soap_response = requests.request( "POST", communication.reservation_id.pms_property_id.ses_url, @@ -1105,6 +1229,8 @@ class TravellerReport(models.TransientModel): communication.processing_result = root.find(".//descripcion").text except requests.exceptions.RequestException as e: _handle_request_exception(communication, e) + except Exception as e: + _handle_request_exception(communication, e) @api.model def create_pending_notifications_traveller_report(self):