mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
[IMP] pms_l10n_es: spanish police travellers report sending
This commit is contained in:
@@ -24,13 +24,14 @@
|
||||
],
|
||||
},
|
||||
"data": [
|
||||
"data/cron_jobs.xml",
|
||||
"data/queue_data.xml",
|
||||
"data/queue_job_function_data.xml",
|
||||
# "data/cron_jobs.xml",
|
||||
# "data/queue_data.xml",
|
||||
# "data/queue_job_function_data.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"views/pms_checkin_partner_views.xml",
|
||||
"views/pms_property_views.xml",
|
||||
"views/res_partner_views.xml",
|
||||
"views/pms_log_institution_traveller_report_views.xml",
|
||||
"wizards/traveller_report.xml",
|
||||
],
|
||||
"installable": True,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from . import res_partner
|
||||
from . import pms_checkin_partner
|
||||
from . import pms_property
|
||||
from . import pms_log_institution_traveller_report
|
||||
|
||||
18
pms_l10n_es/models/pms_log_institution_traveller_report.py
Normal file
18
pms_l10n_es/models/pms_log_institution_traveller_report.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class PmsLogInstitutionTravellerReport(models.Model):
|
||||
_name = "pms.log.institution.traveller.report"
|
||||
_description = "Report of daily sending files of travellers to institutions."
|
||||
|
||||
date = fields.Datetime(
|
||||
string="Date and time",
|
||||
default=fields.Datetime.now,
|
||||
)
|
||||
txtIncidenciesFromInstitution = fields.Text(
|
||||
string="Detailed message",
|
||||
)
|
||||
fileIncidenciesFromInstitution = fields.Binary(
|
||||
string="Detailed file",
|
||||
)
|
||||
txt_filename = fields.Text()
|
||||
@@ -14,7 +14,7 @@ class PmsProperty(models.Model):
|
||||
institution = fields.Selection(
|
||||
[
|
||||
("guardia_civil", "Guardia Civil"),
|
||||
("policia_nacional", "Policía Nacional (soon)"),
|
||||
("policia_nacional", "Policía Nacional"),
|
||||
("ertxaintxa", "Ertxaintxa (soon)"),
|
||||
("mossos", "Mossos_d'esquadra (soon)"),
|
||||
],
|
||||
@@ -24,7 +24,6 @@ class PmsProperty(models.Model):
|
||||
)
|
||||
institution_property_id = fields.Char(
|
||||
string="Institution property id",
|
||||
size=10,
|
||||
help="Id provided by institution to send data from property.",
|
||||
)
|
||||
institution_user = fields.Char(
|
||||
@@ -35,30 +34,27 @@ class PmsProperty(models.Model):
|
||||
help="Password provided by institution to send the data.",
|
||||
)
|
||||
|
||||
def test_gc_connection(self):
|
||||
for pms_property in self:
|
||||
def test_connection(self):
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 "
|
||||
"Build/MRA58N) AppleWebKit/537.36 (KHTML, like "
|
||||
"Gecko) Chrome/90.0.4430.93 Mobile Safari/537.36",
|
||||
}
|
||||
for record in self:
|
||||
if (
|
||||
pms_property.institution == "guardia_civil"
|
||||
and pms_property.institution_property_id
|
||||
and pms_property.institution_user
|
||||
and pms_property.institution_password
|
||||
record.institution == "guardia_civil"
|
||||
and record.institution_property_id
|
||||
and record.institution_user
|
||||
and record.institution_password
|
||||
):
|
||||
|
||||
url = "https://hospederias.guardiacivil.es/"
|
||||
login_route = "/hospederias/login.do"
|
||||
logout_route = "/hospederias/logout.do"
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 "
|
||||
"Build/MRA58N) AppleWebKit/537.36 (KHTML, like "
|
||||
"Gecko) Chrome/90.0.4430.93 Mobile Safari/537.36",
|
||||
}
|
||||
session = requests.session()
|
||||
login_payload = {
|
||||
"usuario": pms_property.institution_user,
|
||||
"pswd": pms_property.institution_password,
|
||||
"usuario": record.institution_user,
|
||||
"pswd": record.institution_password,
|
||||
}
|
||||
|
||||
# login
|
||||
response_login = session.post(
|
||||
url + login_route,
|
||||
@@ -66,7 +62,7 @@ class PmsProperty(models.Model):
|
||||
data=login_payload,
|
||||
verify=get_module_resource("pms_l10n_es", "static", "cert.pem"),
|
||||
)
|
||||
time.sleep(1)
|
||||
time.sleep(0.1)
|
||||
# logout
|
||||
session.get(
|
||||
url + logout_route,
|
||||
@@ -96,3 +92,62 @@ class PmsProperty(models.Model):
|
||||
return message
|
||||
else:
|
||||
raise ValidationError(_("Connection could not be established"))
|
||||
elif (
|
||||
record.institution == "policia_nacional"
|
||||
and record.institution_property_id
|
||||
and record.institution_user
|
||||
and record.institution_password
|
||||
):
|
||||
url = "https://webpol.policia.es/e-hotel"
|
||||
pre_login_route = "/login"
|
||||
login_route = "/execute_login"
|
||||
home_route = "/inicio"
|
||||
logout_route = "/execute_logout"
|
||||
session = requests.session()
|
||||
response_pre_login = session.post(
|
||||
url + pre_login_route,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
)
|
||||
soup = bs(response_pre_login.text, "html.parser")
|
||||
token = soup.select("input[name='_csrf']")[0]["value"]
|
||||
time.sleep(0.1)
|
||||
login_payload = {
|
||||
"username": record.institution_user,
|
||||
"password": record.institution_password,
|
||||
"_csrf": token,
|
||||
}
|
||||
session.post(
|
||||
url + login_route,
|
||||
headers=headers,
|
||||
data=login_payload,
|
||||
verify=False,
|
||||
)
|
||||
time.sleep(0.1)
|
||||
response_home = session.get(
|
||||
url + home_route,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
)
|
||||
soup = bs(response_home.text, "html.parser")
|
||||
login_correct = soup.select("#datosUsuarioBanner")
|
||||
if login_correct:
|
||||
session.post(
|
||||
url + logout_route,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
data={"_csrf": token},
|
||||
)
|
||||
|
||||
message = {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"title": _("Connection Established!"),
|
||||
"message": _("Connection established succesfully"),
|
||||
"sticky": False,
|
||||
},
|
||||
}
|
||||
return message
|
||||
else:
|
||||
raise ValidationError(_("Connection could not be established"))
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
user_access_traveller_report_wizard,user_access_traveller_report_wizard,model_traveller_report_wizard,pms.group_pms_user,1,1,1,1
|
||||
user_access_traveller_report_logs,user_access_traveller_report_logs,model_pms_log_institution_traveller_report,pms.group_pms_user,1,1,1,1
|
||||
|
||||
|
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="pms_log_institution_traveller_report_view_form">
|
||||
<field name="name">pms.log.institution.traveller.report.form</field>
|
||||
<field name="model">pms.log.institution.traveller.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Log institution traveller report detail">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="txt_filename" invisible="1" />
|
||||
<field name="date" />
|
||||
<field name="txtIncidenciesFromInstitution" select="1" />
|
||||
<field
|
||||
name="fileIncidenciesFromInstitution"
|
||||
filename="txt_filename"
|
||||
readonly="1"
|
||||
/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="pms_log_institution_traveller_report_view_tree">
|
||||
<field name="name">pms.log.institution.traveller.report.tree</field>
|
||||
<field name="model">pms.log.institution.traveller.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string=" Property Ubications" create="false">
|
||||
<field name="date" />
|
||||
<field name="txtIncidenciesFromInstitution" />
|
||||
<field name="txt_filename" string="File" />
|
||||
<field
|
||||
name="fileIncidenciesFromInstitution"
|
||||
filename="txt_filename"
|
||||
readonly="1"
|
||||
string="Size"
|
||||
/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
model="ir.actions.act_window"
|
||||
id="open_pms_log_institution_traveller_report_form_tree"
|
||||
>
|
||||
<field name="name">Log of sending files to institutions</field>
|
||||
<field name="res_model">pms.log.institution.traveller.report</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<menuitem
|
||||
name="Log institution traveller report"
|
||||
id="menu_open_pms_log_institution_traveller_report_form_tree"
|
||||
action="open_pms_log_institution_traveller_report_form_tree"
|
||||
parent="pms.menu_reservations"
|
||||
sequence="28"
|
||||
/>
|
||||
</odoo>
|
||||
@@ -27,7 +27,7 @@
|
||||
<field name="institution_password" password="True" />
|
||||
</group>
|
||||
<button
|
||||
name="test_gc_connection"
|
||||
name="test_connection"
|
||||
class="btn btn-primary btn-sm"
|
||||
type="object"
|
||||
string="Test user/password"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import datetime
|
||||
import json
|
||||
import time
|
||||
from datetime import date
|
||||
|
||||
@@ -62,7 +63,6 @@ class TravellerReport(models.TransientModel):
|
||||
[
|
||||
("state", "=", "onboard"),
|
||||
("arrival", ">=", str(date.today()) + " 0:00:00"),
|
||||
("arrival", "<=", str(date.today()) + " 23:59:59"),
|
||||
]
|
||||
):
|
||||
|
||||
@@ -131,32 +131,13 @@ class TravellerReport(models.TransientModel):
|
||||
|
||||
return content
|
||||
|
||||
def send_file_gc(self, pms_property=False):
|
||||
def send_file_gc(self, file_content, called_from_user, pms_property):
|
||||
url = "https://hospederias.guardiacivil.es/"
|
||||
login_route = "/hospederias/login.do"
|
||||
upload_file_route = "/hospederias/cargaFichero.do"
|
||||
logout_route = "/hospederias/logout.do"
|
||||
called_from_user = False
|
||||
if not pms_property:
|
||||
called_from_user = True
|
||||
# get the active property
|
||||
pms_property = self.env["pms.property"].search(
|
||||
[("id", "=", self.env.user.get_active_property_ids()[0])]
|
||||
)
|
||||
|
||||
if not (
|
||||
pms_property
|
||||
and pms_property.institution_property_id
|
||||
and pms_property.institution_user
|
||||
and pms_property.institution_password
|
||||
):
|
||||
raise ValidationError(
|
||||
_("The guest information sending settings is not complete.")
|
||||
)
|
||||
|
||||
content = self.generate_checkin_list(pms_property.id)
|
||||
|
||||
if content:
|
||||
if file_content:
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 "
|
||||
"Build/MRA58N) AppleWebKit/537.36 (KHTML, like "
|
||||
@@ -167,14 +148,13 @@ class TravellerReport(models.TransientModel):
|
||||
"usuario": pms_property.institution_user,
|
||||
"pswd": pms_property.institution_password,
|
||||
}
|
||||
|
||||
# login
|
||||
response_login = session.post(
|
||||
url + login_route,
|
||||
headers=headers,
|
||||
data=login_payload,
|
||||
verify=get_module_resource("pms_l10n_es", "static", "cert.pem"),
|
||||
)
|
||||
time.sleep(0.1)
|
||||
|
||||
# check if authentication was successful / unsuccessful or the
|
||||
# resource cannot be accessed
|
||||
@@ -189,8 +169,7 @@ class TravellerReport(models.TransientModel):
|
||||
raise ValidationError(_("Connection could not be established"))
|
||||
|
||||
# build the file to send
|
||||
files = {"fichero": (pms_property.institution_user + ".999", content)}
|
||||
time.sleep(1)
|
||||
files = {"fichero": (pms_property.institution_user + ".999", file_content)}
|
||||
|
||||
# send file
|
||||
response_file_sent = session.post(
|
||||
@@ -199,8 +178,8 @@ class TravellerReport(models.TransientModel):
|
||||
files=files,
|
||||
verify=get_module_resource("pms_l10n_es", "static", "cert.pem"),
|
||||
)
|
||||
time.sleep(0.1)
|
||||
|
||||
time.sleep(1)
|
||||
# logout & close connection
|
||||
session.get(
|
||||
url + logout_route,
|
||||
@@ -217,8 +196,18 @@ class TravellerReport(models.TransientModel):
|
||||
for e in errors:
|
||||
msg += "Error en línea " + e.select("a")[0].text + ": "
|
||||
msg += e.select("a")[2].text + "\n"
|
||||
self.env["pms.log.institution.traveller.report"].create(
|
||||
{
|
||||
"txt_message": msg,
|
||||
}
|
||||
)
|
||||
raise ValidationError(msg)
|
||||
else:
|
||||
self.env["pms.log.institution.traveller.report"].create(
|
||||
{
|
||||
"txt_message": "Successful file sending",
|
||||
}
|
||||
)
|
||||
if called_from_user:
|
||||
message = {
|
||||
"type": "ir.actions.client",
|
||||
@@ -231,6 +220,227 @@ class TravellerReport(models.TransientModel):
|
||||
}
|
||||
return message
|
||||
|
||||
def send_file_pn(self, file_content, called_from_user, pms_property):
|
||||
base_url = "https://webpol.policia.es"
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 "
|
||||
"Build/MRA58N) AppleWebKit/537.36 (KHTML, like "
|
||||
"Gecko) Chrome/90.0.4430.93 Mobile Safari/537.36",
|
||||
}
|
||||
pre_login_route = "/e-hotel/login"
|
||||
login_route = "/e-hotel/execute_login"
|
||||
next_file_name_route = "/e-hotel/hospederia/ficheros/vista/envioFicheros"
|
||||
upload_file_route = "/e-hotel/hospederia/ficheros/subirFichero"
|
||||
pre_get_list_files_sent_route = (
|
||||
"/e-hotel/hospederia/vista/seguimientoHospederia"
|
||||
)
|
||||
files_sent_list_route = "/e-hotel/hospederia/listar/ficherosHospederia"
|
||||
last_file_errors_route = "/e-hotel/hospederia/report/erroresFicheroHospederia"
|
||||
logout_route = "/e-hotel/execute_logout"
|
||||
|
||||
session = requests.session()
|
||||
|
||||
# retrieve token
|
||||
response_pre_login = session.post(
|
||||
base_url + pre_login_route,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
)
|
||||
time.sleep(0.1)
|
||||
token = bs(response_pre_login.text, "html.parser").select(
|
||||
"input[name='_csrf']"
|
||||
)[0]["value"]
|
||||
# do login
|
||||
session.post(
|
||||
base_url + login_route,
|
||||
headers=headers,
|
||||
data={
|
||||
"username": pms_property.institution_user,
|
||||
"password": pms_property.institution_password,
|
||||
"_csrf": token,
|
||||
},
|
||||
verify=False,
|
||||
)
|
||||
time.sleep(0.1)
|
||||
headers["x-csrf-token"] = token
|
||||
# retrieve file name to send
|
||||
response_name_file_route = session.post(
|
||||
base_url + next_file_name_route,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
)
|
||||
time.sleep(0.1)
|
||||
soup = bs(response_name_file_route.text, "html.parser")
|
||||
file_name = soup.select("#msjNombreFichero > b > u")[0].text
|
||||
# send file
|
||||
session.post(
|
||||
base_url + upload_file_route,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
data={
|
||||
"jsonHiddenComunes": "",
|
||||
"ficheroJ": "",
|
||||
"_csrf": token,
|
||||
},
|
||||
files={"fichero": (file_name, file_content)},
|
||||
)
|
||||
time.sleep(0.1)
|
||||
# retrieve property data
|
||||
response_pre_files_sent_list_route = session.post(
|
||||
base_url + pre_get_list_files_sent_route,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
data={
|
||||
"jsonHiddenComunes": "",
|
||||
"ficheroJ": "",
|
||||
"_csrf": token,
|
||||
},
|
||||
)
|
||||
time.sleep(0.1)
|
||||
soup = bs(response_pre_files_sent_list_route.text, "html.parser")
|
||||
property_specific_data = {
|
||||
"codigoHospederia": soup.select("#codigoHospederia")[0]["value"],
|
||||
"nombreHospederia": soup.select("#nombreHospederia")[0]["value"],
|
||||
"direccionCompleta": soup.select("#direccionCompleta")[0]["value"],
|
||||
"telefono": soup.select("#telefono")[0]["value"],
|
||||
"tieneAgrup": soup.select("#tieneAgrup")[0]["value"],
|
||||
}
|
||||
common_file_data = {
|
||||
"jsonHiddenComunes": "",
|
||||
"fechaDesde": (
|
||||
datetime.date.today() + datetime.timedelta(days=-1)
|
||||
).strftime("%d/%m/%Y"),
|
||||
"fechaHasta": datetime.date.today().strftime("%d/%m/%Y"),
|
||||
"_csrf": token,
|
||||
"_search": False,
|
||||
"nd": str(int(time.time() * 1000)),
|
||||
"rows": 10,
|
||||
"page": 1,
|
||||
"sidx": "",
|
||||
"sord": "dat_fich.fecha_alta desc",
|
||||
}
|
||||
# retrieve list of sent files
|
||||
file_data = dict()
|
||||
for _attempt in range(1, 5):
|
||||
response_files_sent_list_route = session.post(
|
||||
base_url + files_sent_list_route,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
data={
|
||||
**property_specific_data,
|
||||
**common_file_data,
|
||||
"primeraConsulta": True,
|
||||
},
|
||||
)
|
||||
file_list = json.loads(
|
||||
str(bs(response_files_sent_list_route.text, "html.parser"))
|
||||
)["rows"]
|
||||
|
||||
file_data = list(
|
||||
filter(lambda x: x["nombreFichero"] == file_name, file_list)
|
||||
)
|
||||
if file_data:
|
||||
file_data = file_data[0]
|
||||
break
|
||||
else:
|
||||
time.sleep(0.5)
|
||||
|
||||
if not file_data:
|
||||
raise ValidationError(_("Could not send file"))
|
||||
else:
|
||||
response_last_file_errors_route = session.post(
|
||||
base_url + last_file_errors_route,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
data={
|
||||
"idFichero": file_data["idFichero"],
|
||||
"numErroresHuespedes": file_data["numErroresHuespedes"],
|
||||
"numAvisosHuespedes": file_data["numAvisosHuespedes"],
|
||||
"nombreFichero": file_data["nombreFichero"],
|
||||
"fechaAlta": file_data["fechaAlta"],
|
||||
"envioDesdeAgrupacion": file_data["envioDesdeAgrupacion"],
|
||||
"envioDesdeAgrupacionImg": "",
|
||||
"estadoProceso": file_data["estadoProceso"],
|
||||
"numTotalErrores": file_data["numTotalErrores"],
|
||||
"numTotalAvisos": file_data["numTotalAvisos"],
|
||||
"separadorTabla": "",
|
||||
"numHuespedesInformados": file_data["numHuespedesInformados"],
|
||||
"numHuespedes": file_data["numHuespedes"],
|
||||
"primeraConsulta": False,
|
||||
**property_specific_data,
|
||||
**common_file_data,
|
||||
"datosServidor": False,
|
||||
},
|
||||
)
|
||||
|
||||
time.sleep(0.1)
|
||||
soup = bs(response_last_file_errors_route.text, "html.parser")
|
||||
# get file sent pdf report
|
||||
response_last_file_errors_route = session.get(
|
||||
base_url + soup.select("#iframePdf")[0].attrs["src"],
|
||||
headers=headers,
|
||||
verify=False,
|
||||
)
|
||||
time.sleep(0.1)
|
||||
self.env["pms.log.institution.traveller.report"].create(
|
||||
{
|
||||
"fileIncidenciesFromInstitution": base64.b64encode(
|
||||
response_last_file_errors_route.content
|
||||
),
|
||||
"txt_filename": file_name + ".pdf",
|
||||
}
|
||||
)
|
||||
|
||||
# do logout
|
||||
session.post(
|
||||
base_url + logout_route,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
data={"_csrf": token},
|
||||
)
|
||||
|
||||
session.close()
|
||||
# file creation
|
||||
txt_binary = self.env["traveller.report.wizard"].create(
|
||||
{
|
||||
"txt_filename": pms_property.institution_property_id + ".999",
|
||||
"txt_binary": base64.b64encode(response_last_file_errors_route.content),
|
||||
"txt_message": "download pdf report",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"name": _("Traveller Report"),
|
||||
"res_id": txt_binary.id,
|
||||
"res_model": "traveller.report.wizard",
|
||||
"target": "new",
|
||||
"type": "ir.actions.act_window",
|
||||
"view_id": self.env.ref("pms_l10n_es.traveller_report_wizard").id,
|
||||
"view_mode": "form",
|
||||
"view_type": "form",
|
||||
}
|
||||
|
||||
def send_file_institution(self, pms_property=False):
|
||||
called_from_user = False
|
||||
if not pms_property:
|
||||
called_from_user = True
|
||||
pms_property = self.env["pms.property"].search(
|
||||
[("id", "=", self.env.user.get_active_property_ids()[0])]
|
||||
)
|
||||
if not (
|
||||
pms_property
|
||||
and pms_property.institution_property_id
|
||||
and pms_property.institution_user
|
||||
and pms_property.institution_password
|
||||
):
|
||||
raise ValidationError(
|
||||
_("The guest information sending settings is not complete.")
|
||||
)
|
||||
file_content = self.generate_checkin_list(pms_property.id)
|
||||
if pms_property.institution == "policia_nacional":
|
||||
return self.send_file_pn(file_content, called_from_user, pms_property)
|
||||
elif pms_property.institution == "guardia_civil":
|
||||
self.send_file_gc(file_content, called_from_user, pms_property)
|
||||
|
||||
@api.model
|
||||
def send_file_gc_async(self):
|
||||
for prop in self.env["pms.property"].search([]):
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<button
|
||||
name="send_file_gc"
|
||||
name="send_file_institution"
|
||||
class="btn btn-primary btn-sm"
|
||||
type="object"
|
||||
string="Send file"
|
||||
@@ -55,8 +55,8 @@
|
||||
<menuitem
|
||||
id="menu_traveller_report"
|
||||
name="Traveller Report"
|
||||
sequence="30"
|
||||
parent="pms.pms_configuration_menu"
|
||||
sequence="27"
|
||||
parent="pms.menu_reservations"
|
||||
action="action_traveller_report"
|
||||
/>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user