diff --git a/connector_cbl/README.rst b/connector_cbl/README.rst new file mode 100644 index 000000000..32f6e2836 --- /dev/null +++ b/connector_cbl/README.rst @@ -0,0 +1,64 @@ +============= +CBL Connector +============= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:c101f320f89ead1e1b773b9e96d380efa593d9b7d202c1f23f23d8e97d0d7ac4 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-nuobit%2Fodoo--addons-lightgray.png?logo=github + :target: https://github.com/nuobit/odoo-addons/tree/16.0/connector_cbl + :alt: nuobit/odoo-addons + +|badge1| |badge2| |badge3| + +Connector to get tracking info from CBL + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* NuoBiT Solutions SL + +Contributors +~~~~~~~~~~~~ + +* `NuoBiT `__: + + * Eric Antones + * Kilian Niubo + + +Maintainers +~~~~~~~~~~~ + +This module is part of the `nuobit/odoo-addons `_ project on GitHub. + +You are welcome to contribute. diff --git a/connector_cbl/__init__.py b/connector_cbl/__init__.py new file mode 100644 index 000000000..91c5580fe --- /dev/null +++ b/connector_cbl/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/connector_cbl/__manifest__.py b/connector_cbl/__manifest__.py new file mode 100644 index 000000000..61fdda01e --- /dev/null +++ b/connector_cbl/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright NuoBiT Solutions - Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "CBL Connector", + "version": "16.0.1.0.0", + "author": "NuoBiT Solutions SL", + "license": "AGPL-3", + "category": "Connector", + "website": "https://github.com/nuobit/odoo-addons", + "external_dependencies": { + "python": [ + "requests", + "lxml", + ], + }, + "depends": [ + "connector", + ], + "data": [ + "views/backend_views.xml", + "views/menus.xml", + "templates/cbl_template.xml", + "security/ir.model.access.csv", + ], + "installable": True, +} diff --git a/connector_cbl/controllers/__init__.py b/connector_cbl/controllers/__init__.py new file mode 100644 index 000000000..12a7e529b --- /dev/null +++ b/connector_cbl/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/connector_cbl/controllers/main.py b/connector_cbl/controllers/main.py new file mode 100644 index 000000000..de58e3dcd --- /dev/null +++ b/connector_cbl/controllers/main.py @@ -0,0 +1,56 @@ +# Copyright NuoBiT Solutions - Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +import werkzeug.exceptions + +from odoo import http + +from ..models.cbl import CBL + +_logger = logging.getLogger(__name__) + + +class CBLController(http.Controller): + @http.route( + [ + "/tracking/cbl/", + ], + type="http", + auth="public", + ) + def tracking_data(self, tracking_number=None): + remote_ip = http.request.httprequest.environ["REMOTE_ADDR"] + cbl_backend = ( + http.request.env["cbl.backend"] + .sudo() + .search( + [ + ("active", "=", True), + ("state", "=", "checked"), + ] + ) + .sorted(lambda x: x.sequence) + ) + + if not cbl_backend: + return werkzeug.exceptions.InternalServerError("No configuration found") + + er = CBL(username=cbl_backend.username, password=cbl_backend.password) + + _logger.info("Asking %s CBL shipment... from %s" % (tracking_number, remote_ip)) + if not er.login(): + return werkzeug.exceptions.Unauthorized() + + data = er.filter_by_refcte(tracking_number) + if not data: + return werkzeug.exceptions.NotFound( + "There's no data with tracking number '%s'" % tracking_number + ) + _logger.info( + "CBL shipment %s successfully retrieved from %s." + % (tracking_number, remote_ip) + ) + + return http.request.render("connector_cbl.index", {"expeditions": data}) diff --git a/connector_cbl/models/__init__.py b/connector_cbl/models/__init__.py new file mode 100644 index 000000000..8ed188c4e --- /dev/null +++ b/connector_cbl/models/__init__.py @@ -0,0 +1,2 @@ +from . import backend +from . import cbl diff --git a/connector_cbl/models/backend.py b/connector_cbl/models/backend.py new file mode 100644 index 000000000..825900a6e --- /dev/null +++ b/connector_cbl/models/backend.py @@ -0,0 +1,65 @@ +# Copyright NuoBiT Solutions - Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +from . import cbl + +_logger = logging.getLogger(__name__) + + +class CBLBackend(models.Model): + _name = "cbl.backend" + _inherit = "connector.backend" + + _description = "CBL Backend Configuration" + + @api.model + def _select_state(self): + return [ + ("draft", "Draft"), + ("checked", "Checked"), + ("production", "In Production"), + ] + + name = fields.Char( + required=True, + ) + sequence = fields.Integer( + required=True, + default=1, + ) + username = fields.Char( + required=True, + ) + password = fields.Char( + required=True, + ) + output = fields.Text( + readonly=True, + ) + active = fields.Boolean( + default=True, + ) + state = fields.Selection( + selection="_select_state", + default="draft", + ) + + def button_reset_to_draft(self): + self.ensure_one() + self.write({"state": "draft", "output": None}) + + def _check_connection(self): + self.ensure_one() + er = cbl.CBL(username=self.username, password=self.password) + if not er.login(): + raise ValidationError(_("Error on logging in")) + self.output = "OK" + + def button_check_connection(self): + self._check_connection() + self.write({"state": "checked"}) diff --git a/connector_cbl/models/cbl.py b/connector_cbl/models/cbl.py new file mode 100644 index 000000000..22b41420b --- /dev/null +++ b/connector_cbl/models/cbl.py @@ -0,0 +1,231 @@ +# Copyright NuoBiT Solutions - Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import datetime +import json +import logging +import re + +import requests +from lxml import etree + +from odoo import _ +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +def xpath1(tree, xpath): + tag_l = tree.xpath(xpath) + if len(tag_l) == 0: + raise ValidationError(_("No elements found on xpath: %s" % xpath)) + if len(tag_l) != 1: + raise ValidationError( + _( + "Expected 1 element, %(elements)s found on " + "xpath: %(xpath)s" + "Elements:%(tags)s" + % { + "elements": len(tag_l), + "xpath": xpath, + "tags": tag_l, + } + ) + ) + + return tag_l[0] + + +class CBL: + _base_url = "https://clientes.cbl-logistica.com" + + def __init__(self, username, password, debug=False): + self.session = requests.Session() + self.viewstate = None + self.username = username + self.password = password + self.headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/78.0.3904.108 Safari/537.36", + } + self.debug = debug + + def login(self): + url = "%s/login.aspx" % self._base_url + res = self.session.get(url, headers=self.headers) + self._update_viewstate(res) + # login + form_data = { + "ScriptManager1": "UpdatePanel1|Login1$LoginButton", + "Login1$UserName": self.username, + "Login1$Password": self.password, + "Login1$LoginButton": "ENTRAR", + } + form_data.update(self.viewstate) + res = self.session.post(url, data=form_data, headers=self.headers) + self._update_viewstate(res) + # Check login + return not self.is_login_page(res) + + def filter_by_refcte(self, refcte): + url = "%s/Consultas/envios.aspx" % self._base_url + res = self.session.get(url, headers=self.headers) + self._update_viewstate(res) + + userid_tags = [ + "ctl00_TOPCONTENEDOR_WebCUI_def_user", + "ctl00_TOPCONTENEDOR_WebCUI_loginid", + "ctl00_TOPCONTENEDOR_WebCUI_recid", + ] + + tree = etree.HTML(res.text.encode()) + userid = None + for ut in userid_tags: + ut_tag = xpath1(tree, "//form[@id='aspnetForm']//input[@id='%s']" % ut) + if ut_tag is None: + raise ValidationError(_("Expected value")) + if not userid: + userid = ut_tag.attrib["value"] + else: + if userid != ut_tag.attrib["value"]: + raise ValidationError(_("Different userid values")) + + if not userid: + raise ValidationError(_("Userid not found")) + + form_data = { + "ctl00$AJAXScriptManager": "ctl00$UpdatePanel1|ctl00$TOPCONTENEDOR$WebCUI_buscar", + "ctl00$TOPCONTENEDOR$WebCUI_del": "081", + "ctl00$TOPCONTENEDOR$WebCUI_usuario": self.username, + "ctl00$TOPCONTENEDOR$WebCUI_def_user": userid, + "ctl00$TOPCONTENEDOR$WebCUI_loginid": userid, + "ctl00$TOPCONTENEDOR$WebCUI_recid": userid, + "ctl00$TOPCONTENEDOR$WebCUI_lista_f": "SAL", + "ctl00$TOPCONTENEDOR$WebCUI_lista_sit": "TODO", + "ctl00$TOPCONTENEDOR$WebCUI_ref": refcte, + "ctl00$TOPCONTENEDOR$WebCUI_lista_tenv": "TODO", + "ctl00$TOPCONTENEDOR$WebCUI_consulta": "1", + "ctl00$TOPCONTENEDOR$WebCUI_delDest": "000", + "ctl00$TOPCONTENEDOR$WebCUI_lista_grp": "TODOS", + "ctl00$TOPCONTENEDOR$WebCUI_regxpag": "20", + "ctl00$TOPCONTENEDOR$WebCUI_totalpag": "1", + "ctl00$TOPCONTENEDOR$WebCUI_pers_cons": "S", + "ctl00$TOPCONTENEDOR$WebCUI_sit_inc": "N", + "ctl00$TOPCONTENEDOR$WebCUI_total_cons_gr1": "1", + "ctl00$TOPCONTENEDOR$WebCUI_def_cons": "1", + "ctl00$TOPCONTENEDOR$WebCUI_solocte": "S", + "ctl00$TOPCONTENEDOR$WebCUI_diascons": "62", + "ctl00$TOPCONTENEDOR$WebCUI_buscar": "Buscar", + } + form_data.update(self.viewstate) + res = self.session.post(url, data=form_data, headers=self.headers) + self._update_viewstate(res) + + # parse the page + tree = etree.HTML(res.text.encode()) + elem_table_l = tree.xpath("//div[@id='CONSENV_RES']//table") + if len(elem_table_l) == 0: + return [] + elif len(elem_table_l) > 1: + raise ValidationError(_("Unexpected content CONSENV_RES")) + elem_table = elem_table_l[0] + + result_ld = [] + input_detail_l = elem_table.xpath( + "//tr/td/input[contains(@onclick, 'MuestraDetalleConsulta')]" + ) + for input_d in input_detail_l: + m = re.match( + r"^.+MuestraDetalleConsulta\('([^']+)'\)", input_d.attrib["onclick"] + ) + if not m: + raise ValidationError(_("Unexpected content on MuestraDetalleConsulta")) + nexpedicion = m.group(1) + + # Expedition detail + url = "%s/api/Comun/DetalleEnvio/QueryDatosExpedicion" % self._base_url + form_data = { + "expedicion": nexpedicion, + "propietario": "null", + } + res = self.session.post(url, data=form_data, headers=self.headers) + data = res.json()["data"] + expedition_d = data + + # tracking + url = "%s/api/Comun/DetalleEnvio/QueryTracking" % self._base_url + form_data = { + "jtStartIndex": "1", + "jtPageSize": "1", + "jtSorting": None, + "expedicion": nexpedicion, + "propietario": "null", + "usuario": self.username, + } + res = self.session.post(url, data=form_data, headers=self.headers) + data = json.loads(res.json()["data"])["Table"] + for e in data: + e["FECHA"] = datetime.datetime.strptime(e["FECHA"], "%Y-%m-%dT%H:%M:%S") + expedition_d["Tracking"] = data + + result_ld.append(expedition_d) + + return result_ld + + def logout(self): + url = "%s/Default.aspx" % self._base_url + + res = self.session.get(url, headers=self.headers) + self._update_viewstate(res) + + form_data = { + "__EVENTTARGET": "ctl00$LoginStatus1$ctl00", + "__EVENTARGUMENT": "", + "ctl00$jquerylang": "es", + "ctl00$eschromeglobalactivo": "S", + "ctl00$eschromebrowser": "S", + "ctl00$chromeversion": "78", + "ctl00$NoJavaPrint": "N", + } + form_data.update(self.viewstate) + res = self.session.post(url, data=form_data, headers=self.headers) + + return self.is_login_page(res) + + def is_login_page(self, res): + tree = etree.HTML(res.text.encode()) + tag_form_login = tree.xpath("//form[@id='frmlogin']") + + return len(tag_form_login) != 0 + + def _update_viewstate(self, res): + self.viewstate = {} + tree = etree.HTML(res.text.encode()) + viewstate_fields = [ + "__VIEWSTATE", + "__VIEWSTATEGENERATOR", + "__VIEWSTATEENCRYPTED", + "__EVENTVALIDATION", + ] + for vsf in viewstate_fields: + tag_viewstate = tree.xpath("//*[@id='%s']" % vsf) + for t in tag_viewstate: + if "value" not in t.attrib: + raise ValidationError( + _( + "Unexpected, ViewState element iff found must have value attribute" + ) + ) + if vsf not in self.viewstate: + self.viewstate[vsf] = t.attrib["value"] or None + else: + if self.viewstate[vsf] != t.attrib["value"]: + raise ValidationError( + _( + "Unexpected! all the ViewState must have the same 'value'" + ) + ) + + if not self.viewstate: + raise ValidationError(_("ViewState not found")) diff --git a/connector_cbl/readme/CONTRIBUTORS.rst b/connector_cbl/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..c755879ec --- /dev/null +++ b/connector_cbl/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* `NuoBiT `__: + + * Eric Antones + * Kilian Niubo + diff --git a/connector_cbl/readme/DESCRIPTION.rst b/connector_cbl/readme/DESCRIPTION.rst new file mode 100644 index 000000000..a86afa77a --- /dev/null +++ b/connector_cbl/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Connector to get tracking info from CBL diff --git a/connector_cbl/security/ir.model.access.csv b/connector_cbl/security/ir.model.access.csv new file mode 100644 index 000000000..db7b1d844 --- /dev/null +++ b/connector_cbl/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +"access_cbl_backend_manager","cbl_backend connector manager","model_cbl_backend","connector.group_connector_manager",1,1,1,1 diff --git a/connector_cbl/static/description/icon.png b/connector_cbl/static/description/icon.png new file mode 100644 index 000000000..1cd641e79 Binary files /dev/null and b/connector_cbl/static/description/icon.png differ diff --git a/connector_cbl/static/description/index.html b/connector_cbl/static/description/index.html new file mode 100644 index 000000000..549045007 --- /dev/null +++ b/connector_cbl/static/description/index.html @@ -0,0 +1,420 @@ + + + + + + +CBL Connector + + + +
+

CBL Connector

+ + +

Beta License: AGPL-3 nuobit/odoo-addons

+

Connector to get tracking info from CBL

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • NuoBiT Solutions SL
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the nuobit/odoo-addons project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/connector_cbl/templates/cbl_template.xml b/connector_cbl/templates/cbl_template.xml new file mode 100644 index 000000000..451cb090b --- /dev/null +++ b/connector_cbl/templates/cbl_template.xml @@ -0,0 +1,199 @@ + + + +