Merge PR #273 into 14.0

Signed-off-by DarioLodeiros
This commit is contained in:
OCA-git-bot
2024-07-02 17:06:22 +00:00
22 changed files with 16270 additions and 119 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

@@ -39,9 +39,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

@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record model="ir.cron" id="autosend_traveller_report">
<field name="name">Automatic Send Traveller Report</field>
<field name="name">
Automatic Send Traveller Report
</field>
<field name="active" eval="False" />
<field name="interval_number">1</field>
<field name="user_id" ref="base.user_root" />
@@ -12,8 +14,86 @@
<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')"
eval="(datetime.now(pytz.timezone('UTC')) + timedelta(days=1)).strftime('%Y-%m-%d 00:05:00')"
/>
<field name="code">model.send_file_institution_async()</field>
</record>
<record model="ir.cron" id="autocreate_traveller_report_com">
<field name="name">
SES Automatic Creation Traveller Communications
</field>
<field name="active" eval="False" />
<field name="interval_number">1</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_type">days</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 23:30:00')"
/>
<field name="code">
model.create_pending_notifications_traveller_report()
</field>
</record>
<record model="ir.cron" id="autosend_pend_traveller_reports_com">
<field name="name">
SES Automatic Sending Pending Traveller Reports Communications
</field>
<field name="active" eval="False" />
<field name="interval_number">1</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_type">days</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')) + timedelta(days=1)).strftime('%Y-%m-%d 00:05:00')"
/>
<field name="code">model.ses_send_communications('PV')</field>
</record>
<record model="ir.cron" id="autosend_pending_reservation_com">
<field name="name">
SES Automatic Sending Pending Reservation Communications
</field>
<field name="active" eval="False" />
<field name="interval_number">30</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')) + timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')"
/>
<field name="code">model.ses_send_communications('RH')</field>
</record>
<record model="ir.cron" id="autoprocess_sent_com">
<field name="name">
SES Automatic Process Sent Communications
</field>
<field name="active" eval="False" />
<field name="interval_number">30</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')) + timedelta(days=1)).strftime('%Y-%m-%d 00:30: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,102 @@
from odoo import api, fields, models
from ..wizards.traveller_report import CREATE_OPERATION_CODE, DELETE_OPERATION_CODE
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, CREATE_OPERATION_CODE, "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 == CREATE_OPERATION_CODE
):
self.create_communication(
reservation.id, DELETE_OPERATION_CODE, "RH"
)
elif (
vals["state"] != "cancel"
and last_communication.operation == DELETE_OPERATION_CODE
):
self.create_communication(
reservation.id, CREATE_OPERATION_CODE, "RH"
)
elif check_changed:
if last_communication.operation == CREATE_OPERATION_CODE:
self.create_communication(
reservation.id, DELETE_OPERATION_CODE, "RH"
)
self.create_communication(reservation.id, CREATE_OPERATION_CODE, "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

View File

@@ -0,0 +1,44 @@
-----BEGIN CERTIFICATE-----
MIIH0zCCBrugAwIBAgIQajkADBY6ZK1iD4oFQUbYdzANBgkqhkiG9w0BAQsFADBH
MQswCQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xJTAjBgNVBAsMHEFDIENv
bXBvbmVudGVzIEluZm9ybcOhdGljb3MwHhcNMjIwMjE4MTE1OTAxWhcNMjUwMjE4
MTE1OTAwWjCB9zELMAkGA1UEBhMCRVMxDzANBgNVBAcMBk1BRFJJRDFDMEEGA1UE
Cgw6TUlOSVNURVJJTyBJTlRFUklPUiAtIFNFQ1JFVEFSSUEgRVNUQURPIFNFR1VS
SURBRCAtIFNHU0lDUzFEMEIGA1UECww7U1VCLkdFTkVSQUwgU0lTVEVNQVMgSU5G
T1JNQUNJT04gWSBDT01VTklDQUNJT05FUyBTRUdVUklEQUQxEjAQBgNVBAUTCVMy
ODAwMTA5RzEYMBYGA1UEYQwPVkFURVMtUzI4MDAxMDlHMR4wHAYDVQQDDBVQUkUt
U0dTSUNTLlNFUy5NSVIuRVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQCa8OJf1tPWpwnboeomX27292iTcsNONHnZZTnjuV8CUv9t+crTXmv7kA9L6l/1
bcSpkVuWcCnnHjVsn2FnTA1pl82dR/BWlbOx1A8A3d2auUJAIH1uJ68xlGTo1jqp
SZn6z4Ply3zz7Tb09FfkZdWnalp0ozD5y//Rgc91CboNn62lAMe3OLrUI1DWwrVA
gVV7YB7Sx4Tl+wSPu/Qr36f0KmoDW78IqGWjZ6pfJXSZZ9YmIlMBv9IS4GLb1Rh/
JiMLZMRSKdqu1kA7g+dUhy6ueUKlHPzxik2JZIIHydxTHss4kGIFVaZ2LOE8GyuV
AIjYDyl+yJdJm6ROrk5r1kwLAgMBAAGjggQIMIIEBDAMBgNVHRMBAf8EAjAAMIGB
BggrBgEFBQcBAQR1MHMwOwYIKwYBBQUHMAGGL2h0dHA6Ly9vY3NwY29tcC5jZXJ0
LmZubXQuZXMvb2NzcC9PY3NwUmVzcG9uZGVyMDQGCCsGAQUFBzAChihodHRwOi8v
d3d3LmNlcnQuZm5tdC5lcy9jZXJ0cy9BQ0NPTVAuY3J0MIIBNAYDVR0gBIIBKzCC
AScwggEYBgorBgEEAaxmAwkTMIIBCDApBggrBgEFBQcCARYdaHR0cDovL3d3dy5j
ZXJ0LmZubXQuZXMvZHBjcy8wgdoGCCsGAQUFBwICMIHNDIHKQ2VydGlmaWNhZG8g
Y3VhbGlmaWNhZG8gZGUgc2VsbG8gZWxlY3Ryw7NuaWNvIHNlZ8O6biByZWdsYW1l
bnRvIGV1cm9wZW8gZUlEQVMuIFN1amV0byBhIGxhcyBjb25kaWNpb25lcyBkZSB1
c28gZXhwdWVzdGFzIGVuIGxhIERQQyBkZSBGTk1ULVJDTSBjb24gTklGOiBRMjgy
NjAwNC1KIChDL0pvcmdlIEp1YW4gMTA2LTI4MDA5LU1hZHJpZC1Fc3Bhw7FhKTAJ
BgcEAIvsQAEBMDMGA1UdEQQsMCqkKDAmMSQwIgYJKwYBBAGsZgEIDBVQUkUtU0dT
SUNTLlNFUy5NSVIuRVMwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMA4G
A1UdDwEB/wQEAwIF4DAdBgNVHQ4EFgQURXqQD9HjmqRh4SuG9Zf8Mh8YUukwgbAG
CCsGAQUFBwEDBIGjMIGgMAgGBgQAjkYBATALBgYEAI5GAQMCAQ8wEwYGBACORgEG
MAkGBwQAjkYBBgIwcgYGBACORgEFMGgwMhYsaHR0cHM6Ly93d3cuY2VydC5mbm10
LmVzL3Bkcy9QRFNfQ09NUF9lcy5wZGYTAmVzMDIWLGh0dHBzOi8vd3d3LmNlcnQu
Zm5tdC5lcy9wZHMvUERTX0NPTVBfZW4ucGRmEwJlbjAfBgNVHSMEGDAWgBQZ+Fgv
FNamzJsEmAgNTNerAKeDZTCB4AYDVR0fBIHYMIHVMIHSoIHPoIHMhoGebGRhcDov
L2xkYXBjb21wLmNlcnQuZm5tdC5lcy9DTj1DUkwxLE9VPUFDJTIwQ29tcG9uZW50
ZXMlMjBJbmZvcm1hdGljb3MsTz1GTk1ULVJDTSxDPUVTP2NlcnRpZmljYXRlUmV2
b2NhdGlvbkxpc3Q7YmluYXJ5P2Jhc2U/b2JqZWN0Y2xhc3M9Y1JMRGlzdHJpYnV0
aW9uUG9pbnSGKWh0dHA6Ly93d3cuY2VydC5mbm10LmVzL2NybHNjb21wL0NSTDEu
Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQAsgDOnmLs0lyBc29ABc6aCUIdxDLxbSodp
0rBMxKl3G6fGakUXJYCD9GBKRNMmCD8NXuYtNU6KmSWeXXUvz9ixGB234wKVdKx3
en6j0mmiJWwTmMP6KNZ77/B/6GXmV1Wum+iyqKBG4NEy42OSF57sCChD2toLfcWP
7to3i30GUzaWgeISFSC1Lq7iqbKLZzMjlh82AqD8F9Xvo+N9YadxQ51bPPt+4WNW
r4oj6PM8w6nhVEGW7+h5hpTFgt7O0H9G6U5D8N+utHe9dKssGvJLKpATzJvMjPxS
pU3eaujFO6D0qhmInyV+PtjSIhL8hzeTQcAXR6oBqNtOrplcTahz
-----END CERTIFICATE-----

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)
@@ -619,7 +652,7 @@ class TestWizardINE(TestPms):
},
}
# ACT
nationalities = self.env["pms.ine.wizard"].ine_nationalities(
nationalities = self.env["pms.ine.wizard"].ine_countries(
start_date, end_date, self.pms_property1.id
)
# ASSERT
@@ -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
@@ -745,7 +778,7 @@ class TestWizardINE(TestPms):
}
}
# ACT
nationalities = self.env["pms.ine.wizard"].ine_nationalities(
nationalities = self.env["pms.ine.wizard"].ine_countries(
start_date, end_date, self.pms_property1.id
)
# ASSERT
@@ -877,7 +910,7 @@ class TestWizardINE(TestPms):
},
}
# ACT
nationalities = self.env["pms.ine.wizard"].ine_nationalities(
nationalities = self.env["pms.ine.wizard"].ine_countries(
start_date, end_date, self.pms_property1.id
)
# ASSERT
@@ -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)
@@ -923,7 +956,7 @@ class TestWizardINE(TestPms):
ValidationError,
msg="Cannot generate INE if some checkin partner has no nationality",
):
self.env["pms.ine.wizard"].ine_nationalities(
self.env["pms.ine.wizard"].ine_countries(
start_date, end_date, self.pms_property1.id
)
@@ -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
@@ -1009,6 +1042,6 @@ class TestWizardINE(TestPms):
ValidationError,
msg="Cannot generate INE if some checkin partner from Spain has no nationality",
):
self.env["pms.ine.wizard"].ine_nationalities(
self.env["pms.ine.wizard"].ine_countries(
start_date, end_date, self.pms_property1.id
)

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,9 +1,13 @@
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
@@ -15,6 +19,312 @@ 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"
CREATE_OPERATION_CODE = "A"
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 = (
f"Cannot establish the connection. ({e.args})"
)
else:
communication.processing_result = (
f"Cannot establish the connection. ({e.args})"
)
elif isinstance(e, requests.exceptions.Timeout):
if communication.state == "to_send":
communication.sending_result = (
f"The request took too long to complete. ({e.args})"
)
else:
communication.processing_result = (
f"The request took too long to complete. ({e.args})"
)
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 +332,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 +359,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 +400,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 +847,292 @@ 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=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)
@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=get_module_resource("pms_l10n_es", "static", "cert.pem"),
)
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,
CREATE_OPERATION_CODE,
"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

@@ -1,6 +1,7 @@
import base64
import calendar
import datetime
import math
import xml.etree.cElementTree as ET
from odoo import _, api, fields, models
@@ -200,7 +201,7 @@ class WizardIne(models.TransientModel):
return rooms
@api.model
def ine_nationalities(self, start_date, end_date, pms_property_id):
def ine_countries(self, start_date, end_date, pms_property_id):
"""
Returns a dictionary:
{
@@ -238,39 +239,39 @@ class WizardIne(models.TransientModel):
"""
for entry in read_group_result:
if not entry["nationality_id"]:
guests_with_no_nationality = self.env["pms.checkin.partner"].search(
entry["__domain"]
)
guests_with_no_nationality = (
str(guests_with_no_nationality.mapped("name"))
if not entry["residence_country_id"]:
guests_with_no_residence_country = self.env[
"pms.checkin.partner"
].search(entry["__domain"])
guests_with_no_residence_country = (
str(guests_with_no_residence_country.mapped("name"))
.replace("[", "")
.replace("]", "")
)
raise ValidationError(
_(
"The following guests have no residence nationality set :%s.",
guests_with_no_nationality,
"The following guests have no residence country set :%s.",
guests_with_no_residence_country,
)
)
# 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 countries.get(residence_country_id_code):
countries[residence_country_id_code] = dict()
if not countries[residence_country_id_code].get(date):
countries[residence_country_id_code][date] = dict()
countries[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(
@@ -312,20 +313,18 @@ class WizardIne(models.TransientModel):
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 countries.get(CODE_SPAIN):
countries[CODE_SPAIN] = dict()
if not nationalities[CODE_SPAIN].get(ine_code):
nationalities[CODE_SPAIN][ine_code] = dict()
if not countries[CODE_SPAIN].get(ine_code):
countries[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
if not countries[CODE_SPAIN][ine_code].get(date):
countries[CODE_SPAIN][ine_code][date] = dict()
countries[CODE_SPAIN][ine_code][date][type_of_entry] = num_spain
# result object
nationalities = dict()
countries = dict()
# iterate days between start_date and end_date
for p_date in [
@@ -351,36 +350,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(
@@ -393,7 +392,7 @@ class WizardIne(models.TransientModel):
p_date, "pernoctations", read_by_pernoctations
)
return nationalities
return countries
def ine_calculate_adr(self, start_date, end_date, domain=False):
"""
@@ -575,10 +574,10 @@ class WizardIne(models.TransientModel):
# INE XML -> GUESTS
accommodation_tag = ET.SubElement(survey_tag, "ALOJAMIENTO")
nationalities = self.ine_nationalities(
countries = self.ine_countries(
self.start_date, self.end_date, self.pms_property_id.id
)
for key_country, value_country in nationalities.items():
for key_country, value_country in countries.items():
country = self.env["res.country"].search([("code", "=", key_country)])
@@ -741,8 +740,21 @@ class WizardIne(models.TransientModel):
# so at least I will feel that the effort made some sense :)
total_percent = sum([val for val in percents.values()])
sum_percentages = 0
for group in total_groups_domains.keys():
percents[group] = round(percents[group] * 100 / (total_percent or 1), 2)
sum_percentages += percents[group]
if sum_percentages < 100:
for group in total_groups_domains.keys():
if percents[group] > 0:
percents[group] += math.ceil((100 - sum_percentages) * 100) / 100
break
elif sum_percentages > 100:
for group in total_groups_domains.keys():
if percents[group] > 0:
percents[group] -= math.ceil((sum_percentages - 100) * 100) / 100
break
ET.SubElement(prices_tag, "ADR_TOUROPERADOR_TRADICIONAL").text = str(
adrs["tour_operator_offline"]