diff --git a/oca_dependencies.txt b/oca_dependencies.txt index b35b50ed59..db37c6a913 100644 --- a/oca_dependencies.txt +++ b/oca_dependencies.txt @@ -10,3 +10,4 @@ stock-logistics-workflow connector storage server-auth +web diff --git a/punchout/__init__.py b/punchout/__init__.py new file mode 100644 index 0000000000..46cdc88e41 --- /dev/null +++ b/punchout/__init__.py @@ -0,0 +1,3 @@ +from . import controllers +from . import mixins +from . import models diff --git a/punchout/__manifest__.py b/punchout/__manifest__.py new file mode 100644 index 0000000000..c1a9c3d90e --- /dev/null +++ b/punchout/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Punchout", + "version": "13.0.1.0.0", + "license": "AGPL-3", + "author": "Odoo Community Association (OCA), ACSONE SA/NV", + "website": "https://github.com/OCA/edi", + "depends": [ + # odoo addons + "base", + # OCA/web + "web_notify", + ], + "data": [ + "security/punchout_backend.xml", + "security/punchout_request.xml", + "views/punchout_backend.xml", + "views/punchout_request.xml", + ], + "external_dependencies": {"python": ["cryptography", "lxml"]}, +} diff --git a/punchout/controllers/__init__.py b/punchout/controllers/__init__.py new file mode 100644 index 0000000000..12a7e529b6 --- /dev/null +++ b/punchout/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/punchout/controllers/main.py b/punchout/controllers/main.py new file mode 100644 index 0000000000..abc18b8245 --- /dev/null +++ b/punchout/controllers/main.py @@ -0,0 +1,34 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +import werkzeug + +from odoo.http import Controller, request, route + +_logger = logging.getLogger(__name__) + + +class PunchoutController(Controller): + @route( + "/punchout/cxml/receive/", + type="http", + auth="user", + methods=["POST"], + csrf=False, + ) + def receive_punchout_response(self, backend_id, *args, **kwargs): + cxml_string = kwargs.get("cxml-urlencoded") + punchout_request = ( + request.env["punchout.request"] + .sudo() + ._store_punchout_request(backend_id, cxml_string) + ) + if not punchout_request: + _logger.error( + "Unable to link the punchout response to a punchout.request " + "with given XML: \n%s", + cxml_string, + ) + return werkzeug.utils.redirect("/web") diff --git a/punchout/i18n/fr.po b/punchout/i18n/fr.po new file mode 100644 index 0000000000..49dc716521 --- /dev/null +++ b/punchout/i18n/fr.po @@ -0,0 +1,409 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * punchout +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-26 13:26+0000\n" +"PO-Revision-Date: 2023-09-26 13:26+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: punchout +#: model_terms:ir.ui.view,arch_db:punchout.punchout_backend_form_view +msgid "Access" +msgstr "Accéder" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__action_process_allowed +msgid "Action Process Allowed" +msgstr "" + +#. module: punchout +#: model_terms:ir.ui.view,arch_db:punchout.punchout_request_form_view +msgid "An error occured during the process of the request." +msgstr "Une erreur est survenue lors du traitement de la requête." + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__backend_id +msgid "Backend" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__browser_form_post_url +msgid "Browser form post URL" +msgstr "" + +#. module: punchout +#: code:addons/punchout/models/punchout_backend.py:0 +#, python-format +msgid "Browser form post url is not configured on " +msgstr "L'URL browser form post n'est pas configurée sur " + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__buyer_cookie_id +msgid "Cookie" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__create_uid +#: model:ir.model.fields,field_description:punchout.field_punchout_request__create_uid +msgid "Created by" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__create_date +#: model:ir.model.fields,field_description:punchout.field_punchout_request__create_date +msgid "Created on" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__deployment_mode +#: model_terms:ir.ui.view,arch_db:punchout.punchout_backend_search_view +msgid "Deployment mode" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__display_name +#: model:ir.model.fields,field_description:punchout.field_punchout_request__display_name +#: model:ir.model.fields,field_description:punchout.field_punchout_request_mixin__display_name +msgid "Display Name" +msgstr "" + +#. module: punchout +#: code:addons/punchout/models/punchout_request.py:0 +#, python-format +msgid "Done" +msgstr "Tratiée" + +#. module: punchout +#: code:addons/punchout/models/punchout_request.py:0 +#, python-format +msgid "Draft" +msgstr "Brouillon" + +#. module: punchout +#: model_terms:ir.ui.view,arch_db:punchout.punchout_request_form_view +msgid "Erreur" +msgstr "" + +#. module: punchout +#: code:addons/punchout/models/punchout_request.py:0 +#, python-format +msgid "Error" +msgstr "Erreur" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__error_message +msgid "Error Message" +msgstr "Message d'erreur" + +#. module: punchout +#: model:ir.model.fields,help:punchout.field_punchout_backend__browser_form_post_url +msgid "Exposed URL where the shopping cart must be sent back to." +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_channel_ids +msgid "Followers (Channels)" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__from_domain +msgid "From domain" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__from_identity +msgid "From identity" +msgstr "" + +#. module: punchout +#: model_terms:ir.ui.view,arch_db:punchout.punchout_backend_search_view +msgid "Group By" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__id +#: model:ir.model.fields,field_description:punchout.field_punchout_request__id +#: model:ir.model.fields,field_description:punchout.field_punchout_request_mixin__id +msgid "ID" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,help:punchout.field_punchout_request__message_needaction +#: model:ir.model.fields,help:punchout.field_punchout_request__message_unread +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: punchout +#: model:ir.model.fields,help:punchout.field_punchout_request__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__buyer_cookie_encryption_key +msgid "Key to encrypt the buyer cookie" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend____last_update +#: model:ir.model.fields,field_description:punchout.field_punchout_request____last_update +#: model:ir.model.fields,field_description:punchout.field_punchout_request_mixin____last_update +msgid "Last Modified on" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__write_uid +#: model:ir.model.fields,field_description:punchout.field_punchout_request__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__write_date +#: model:ir.model.fields,field_description:punchout.field_punchout_request__write_date +msgid "Last Updated on" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_ids +msgid "Messages" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__name +msgid "Name" +msgstr "" + +#. module: punchout +#: model_terms:ir.ui.view,arch_db:punchout.punchout_backend_form_view +msgid "Networkid" +msgstr "" + +#. module: punchout +#: code:addons/punchout/models/punchout_request.py:0 +#, python-format +msgid "No punchout backend found to initialize the connection." +msgstr "Aucun backend punchout trouvé pour initialisé la connexion." + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,help:punchout.field_punchout_request__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,help:punchout.field_punchout_request__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,help:punchout.field_punchout_request__message_unread_counter +msgid "Number of unread messages" +msgstr "" + +#. module: punchout +#: model_terms:ir.ui.view,arch_db:punchout.punchout_request_form_view +msgid "Process" +msgstr "Traiter" + +#. module: punchout +#: code:addons/punchout/models/punchout_request.py:0 +#, python-format +msgid "Processing the request" +msgstr "Traitement de la requête" + +#. module: punchout +#: model:ir.ui.menu,name:punchout.punchout_menu +msgid "PunchOut" +msgstr "" + +#. module: punchout +#: model:ir.model,name:punchout.model_punchout_backend +#: model_terms:ir.ui.view,arch_db:punchout.punchout_backend_form_view +msgid "PunchOut Backend" +msgstr "Backend PunchOut" + +#. module: punchout +#: model:ir.ui.menu,name:punchout.punchout_backend_menu +msgid "PunchOut Backends" +msgstr "Backends PunchOut" + +#. module: punchout +#: model:ir.actions.act_window,name:punchout.punchout_request_act_window +#: model:ir.ui.menu,name:punchout.punchout_request_menu +msgid "PunchOut Requests" +msgstr "Requêtes PunchOut" + +#. module: punchout +#: code:addons/punchout/models/punchout_request.py:0 +#: code:addons/punchout/models/punchout_request.py:0 +#, python-format +msgid "PunchOut request error" +msgstr "" + +#. module: punchout +#: model:ir.actions.act_window,name:punchout.punchout_backend_act_window +msgid "Punchout Backends" +msgstr "" + +#. module: punchout +#: model:ir.model,name:punchout.model_punchout_request +msgid "Punchout Request" +msgstr "Requête Punchout " + +#. module: punchout +#: model:ir.model,name:punchout.model_punchout_request_mixin +msgid "Punchout Request (mixin)" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__cxml_response +#: model_terms:ir.ui.view,arch_db:punchout.punchout_request_form_view +msgid "Response" +msgstr "Réponse" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__cxml_response_date +msgid "Response Date" +msgstr "Date de la réponse" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__shared_secret +msgid "Shared secret" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__punchout_url +msgid "Start URL" +msgstr "URL de départ" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__state +msgid "State" +msgstr "Statut" + +#. module: punchout +#: model:ir.model.fields,help:punchout.field_punchout_backend__deployment_mode +msgid "Test or production" +msgstr "" + +#. module: punchout +#: code:addons/punchout/models/punchout_request.py:0 +#, python-format +msgid "The response has been received and will be processed." +msgstr "La réponse a été reçue et va être traitée." + +#. module: punchout +#: code:addons/punchout/models/punchout_backend.py:0 +#: model:ir.model.constraint,message:punchout.constraint_punchout_backend_name_unique +#, python-format +msgid "This PunchOut backend already exists." +msgstr "" + +#. module: punchout +#: code:addons/punchout/models/punchout_request.py:0 +#, python-format +msgid "To Process" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__to_domain +msgid "To domain" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__to_identity +msgid "To identity" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__url +msgid "URL" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_unread +msgid "Unread Messages" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__message_unread_counter +msgid "Unread Messages Counter" +msgstr "" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_request__user_id +msgid "User" +msgstr "Utilisateur" + +#. module: punchout +#: model:ir.model.fields,field_description:punchout.field_punchout_backend__user_agent +msgid "User agent" +msgstr "" + +#. module: punchout +#: code:addons/punchout/models/punchout_request.py:0 +#, python-format +msgid "You are not allowed to process this request. " +msgstr "" + +#. module: punchout +#: code:addons/punchout/models/punchout_request.py:0 +#, python-format +msgid "" +"You must set a personnal email in your preferences in order to access this " +"feature." +msgstr "" +"Vous devez configurer un email personnel dans vos préférences pour utiliser " +"cette fonctionnalité." \ No newline at end of file diff --git a/punchout/mixins/__init__.py b/punchout/mixins/__init__.py new file mode 100644 index 0000000000..c28d0df313 --- /dev/null +++ b/punchout/mixins/__init__.py @@ -0,0 +1 @@ +from . import punchout_request_mixin diff --git a/punchout/mixins/punchout_request_mixin.py b/punchout/mixins/punchout_request_mixin.py new file mode 100644 index 0000000000..e0c4dea675 --- /dev/null +++ b/punchout/mixins/punchout_request_mixin.py @@ -0,0 +1,25 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, models + +_logger = logging.getLogger(__name__) + + +class PunchoutRequest(models.AbstractModel): + _name = "punchout.request.mixin" + _description = "Punchout Request (mixin)" + + @api.model + def _redirect_to_punchout(self): + self._check_access_punchout() + return self.env["punchout.request"]._redirect_to_punchout() + + def _check_access_punchout(self): + """ + Inherit this method to check if current user can access + the punchout website depending on the linked model + """ + return True diff --git a/punchout/models/__init__.py b/punchout/models/__init__.py new file mode 100644 index 0000000000..cff0fd5f1d --- /dev/null +++ b/punchout/models/__init__.py @@ -0,0 +1,2 @@ +from . import punchout_backend +from . import punchout_request diff --git a/punchout/models/punchout_backend.py b/punchout/models/punchout_backend.py new file mode 100644 index 0000000000..0dab8c579a --- /dev/null +++ b/punchout/models/punchout_backend.py @@ -0,0 +1,69 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models +from odoo.exceptions import UserError + + +class PunchoutBackend(models.Model): + _name = "punchout.backend" + _description = "PunchOut Backend" + _sql_constraints = [ + ("name_unique", "unique(name)", _("This PunchOut backend already exists.")) + ] + + name = fields.Char(string="Name", required=True,) + url = fields.Char(string="URL", required=True,) + from_domain = fields.Char(string="From domain", required=True,) + from_identity = fields.Char(string="From identity", required=True,) + to_domain = fields.Char(string="To domain", required=True,) + to_identity = fields.Char(string="To identity", required=True,) + shared_secret = fields.Char(string="Shared secret", required=True,) + user_agent = fields.Char(string="User agent", required=True,) + deployment_mode = fields.Char( + string="Deployment mode", help="Test or production", required=True, + ) + browser_form_post_url = fields.Char( + string="Browser form post URL", + help="Exposed URL where the shopping cart must be sent back to.", + required=True, + ) + buyer_cookie_encryption_key = fields.Char( + string="Key to encrypt the buyer cookie", required=True, + ) + + def _get_domain_and_identity(self, credential_type): + self.ensure_one() + if credential_type in ("From", "Sender"): + return self.from_domain, self.from_identity + if credential_type == "To": + return self.to_domain, self.to_identity + return False, False + + def _get_browser_form_post_url(self): + self.ensure_one() + url = self.browser_form_post_url + if url and url.startswith("http://") or url.startswith("https://"): + return url + base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + if base_url.endswith("/"): + base_url = "".join(base_url[:-1]) + if url and url.startswith("/"): + url = "".join(url[1:]) + if not url: + raise UserError( + _( + f"Browser form post url is not configured on " + f"the backend. {self.display_name}" + ) + ) + + return "/".join([base_url, url, str(self.id)]) + + def redirect_to_backend(self): + self.ensure_one() + return ( + self.env["punchout.request"] + .with_context(punchout_backend_id=self.id,) + ._redirect_to_punchout() + ) diff --git a/punchout/models/punchout_request.py b/punchout/models/punchout_request.py new file mode 100644 index 0000000000..64bcf7c8d0 --- /dev/null +++ b/punchout/models/punchout_request.py @@ -0,0 +1,343 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +import os +import random +import time +from datetime import datetime +from urllib.parse import urlparse +from uuid import uuid4 + +import lxml.etree as ET +import pytz +import requests +from cryptography.fernet import Fernet, InvalidToken + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class PunchoutRequest(models.Model): + _name = "punchout.request" + _inherit = [ + "mail.thread", + ] + _order = "create_date desc" + _description = "Punchout Request" + + backend_id = fields.Many2one(comodel_name="punchout.backend", readonly=True,) + user_id = fields.Many2one( + comodel_name="res.users", readonly=True, default=lambda self: self.env.uid, + ) + buyer_cookie_id = fields.Char(readonly=True, string="Cookie") + punchout_url = fields.Char(readonly=True, string="Start URL") + cxml_response = fields.Text(readonly=True, string="Response",) + cxml_response_date = fields.Datetime(readonly=True, string="Response Date",) + error_message = fields.Text(readonly=True,) + state = fields.Selection( + selection="_selection_state", default="draft", tracking=True, + ) + action_process_allowed = fields.Boolean(compute="_compute_action_process_allowed") + + @api.depends("state",) + def _compute_action_process_allowed(self): + for rec in self: + rec.action_process_allowed = rec.state in ("to_process", "error") + + @api.model + def _selection_state(self): + return [ + ("draft", _("Draft")), + ("error", _("Error")), + ("to_process", _("To Process")), + ("done", _("Done")), + ] + + @api.model + def _get_punchout_request_timestamp(self): + """ + Get ISO 8601 timestamp + """ + current_time = datetime.now() + timezone = pytz.timezone(self.env.user.tz) + localized_time = current_time.astimezone(timezone) + return localized_time.strftime("%Y-%m-%dT%H:%M:%S%z") + + def _get_punchout_payload_identity(self): + ir_config_parameter_model = self.env["ir.config_parameter"].sudo() + base_url = ir_config_parameter_model.get_param("web.base.url") + parsed_url = urlparse(base_url) + domain = parsed_url.netloc + timestamp = int(time.time()) + pid = "{:03d}".format(os.getpid()) + random_numbers_list = [random.randint(0, 9) for _ in range(5)] + random_numbers = "".join(map(str, random_numbers_list)) + return f"{timestamp}{pid}{random_numbers}@{domain}" + + @api.model + def _get_punchout_request_header_credential(self, backend, header, credential_type): + """ + Adds the asked credential tag in the header. + :param backend: punchout.backend + :param header: xml.etree.ElementTree.Element (header) + :param credential_type: "From", "To" or "Sender" + :return: xml.etree.ElementTree.Element (header) + """ + header_cred_type = ET.SubElement(header, credential_type) + cred_domain, cred_identity = backend._get_domain_and_identity(credential_type) + header_cred = ET.SubElement( + header_cred_type, "Credential", attrib={"domain": cred_domain} + ) + ET.SubElement(header_cred, "Identity").text = cred_identity + if credential_type == "Sender": + ET.SubElement(header_cred, "SharedSecret").text = backend.shared_secret + ET.SubElement(header_cred_type, "UserAgent").text = backend.user_agent + return header + + def _get_punchout_request_header(self, cxml, backend): + """ + Adds the header tag in the cXML PunchOut request element. + :param cxml: xml.etree.ElementTree.Element (cxml) + :param backend: punchout.backend + :return: xml.etree.ElementTree.Element (cxml) + """ + header = ET.SubElement(cxml, "Header") + self._get_punchout_request_header_credential(backend, header, "From") + self._get_punchout_request_header_credential(backend, header, "To") + self._get_punchout_request_header_credential(backend, header, "Sender") + return cxml + + def _get_punchout_buyer_cookie(self): + return f"{self.env.user.id}-{uuid4()}" + + def _get_encrypted_punchout_buyer_cookie(self, backend, buyer_cookie): + encyrption_key = backend.buyer_cookie_encryption_key.encode() + return Fernet(encyrption_key).encrypt(buyer_cookie.encode("utf-8")) + + def _get_punchout_request_user_email(self): + return self.env.user.email + + def _get_punchout_request_setup(self, backend, buyer_cookie_id): + payload_id = self._get_punchout_payload_identity() + request_timestamp = self._get_punchout_request_timestamp() + encrypted_buyer_cookie_id = self._get_encrypted_punchout_buyer_cookie( + backend, buyer_cookie_id + ) + user_email = self._get_punchout_request_user_email() + if not user_email: + raise UserError( + _( + "You must set a personnal email in your preferences " + "in order to access this feature." + ) + ) + cxml = ET.Element( + "cXML", + attrib={ + "version": "1.2.008", + "{http://www.w3.org/XML/1998/namespace}lang": "en", + "payloadID": payload_id, + "timestamp": request_timestamp, + }, + ) + self._get_punchout_request_header(cxml, backend) + + deployment_mode = backend.deployment_mode + request = ET.SubElement( + cxml, "Request", attrib={"deploymentMode": deployment_mode} + ) + setup_request = ET.SubElement( + request, "PunchOutSetupRequest", attrib={"operation": "create"} + ) + ET.SubElement(setup_request, "BuyerCookie").text = encrypted_buyer_cookie_id + ET.SubElement( + setup_request, "Extrinsic", attrib={"name": "UserEmail"} + ).text = user_email + browser_form_post = ET.SubElement(setup_request, "BrowserFormPost") + ET.SubElement( + browser_form_post, "URL" + ).text = backend._get_browser_form_post_url() + return cxml + + @api.model + def _check_punchout_request_ok(self, response): + res = True + response_tree = ET.fromstring(response.content) + cxml_status_code = 0 + cxml_status_text = "" + for cxml_status in response_tree.findall("./Response/Status"): + cxml_status_code = int(cxml_status.attrib.get("code", 0)) + cxml_status_text = cxml_status.attrib.get("text", "") + if not response.ok: + log_msg = ( + f"PunchOut {self._name}: {response.status_code}: " + f"{response.reason}\n" + f"URL: {response.url}" + ) + _logger.error(log_msg) + self.env.user.notify_warning( + title=_("PunchOut request error"), + message=_( + f"The PunchOut request with URL {response.url} returned " + f"{response.status_code} ({response.reason})." + ), + sticky=True, + ) + res = False + if not 200 <= cxml_status_code <= 400: + log_msg = ( + f"PunchOut {self._name}: cXML {cxml_status_code}: " + f"{cxml_status_text}\n" + f"URL: {response.url}" + ) + _logger.error(log_msg) + self.env.user.notify_warning( + title=_("PunchOut request error"), + message=_( + f"The PunchOut request with URL {response.url} returned " + f"cXML {cxml_status_code} ({cxml_status_text})." + ), + sticky=True, + ) + res = False + return res + + def _get_post_punchout_setup_url(self, punchout_backend, buyer_cookie_id): + punchout_setup_url = punchout_backend.url + cxml_request_element = self._get_punchout_request_setup( + punchout_backend, buyer_cookie_id + ) + cxml_request_str = ET.tostring( + cxml_request_element, + encoding="UTF-8", + xml_declaration=True, + pretty_print=True, + doctype='', + ).decode("utf-8") + _logger.info("PunchOut %s: posting setup request", self._name) + response = requests.post( + punchout_setup_url, + data=cxml_request_str, + headers={"Content-Type": "text/xml"}, + timeout=30, + ) + if not self._check_punchout_request_ok(response): + return {} + response_tree = ET.fromstring(response.content) + start_page_url = "" + for url in response_tree.findall( + "./Response/PunchOutSetupResponse/StartPage/URL" + ): + start_page_url = url.text + return start_page_url + + @api.model + def _redirect_to_punchout(self): + request = self.sudo()._create_punchout_request() + return { + "type": "ir.actions.act_url", + "url": request.punchout_url, + "target": "new", + } + + @api.model + def _create_punchout_request(self): + punchout_backend = self._get_punchout_backend_to_use() + buyer_cookie_id = self._get_punchout_buyer_cookie() + url = self._get_post_punchout_setup_url(punchout_backend, buyer_cookie_id) + return self.env["punchout.request"].create( + { + "user_id": self.env.user.id, + "punchout_url": url, + "buyer_cookie_id": buyer_cookie_id, + "backend_id": punchout_backend.id, + } + ) + + @api.model + def _get_punchout_backend_to_use(self): + punchout_backend_model = self.env["punchout.backend"] + punchout_backend_id = self.env.context.get("punchout_backend_id") + if punchout_backend_id: + backend = punchout_backend_model.browse(punchout_backend_id) + else: + backend = punchout_backend_model.search([], limit=1) + if not backend: + raise UserError( + _("No punchout backend found to initialize the connection.") + ) + return backend + + @api.model + def _store_punchout_request(self, backend_id, cxml_string): + backend = self.env["punchout.backend"].sudo().browse(backend_id) + tree = ET.fromstring(cxml_string.encode()) + buyer_cookie = tree.find(".//BuyerCookie") + encrypted_buyer_cookie_id = ( + buyer_cookie.text.strip() if buyer_cookie is not None else False + ) + if not encrypted_buyer_cookie_id: + _logger.error( + "Unable to find a buyer cookie from the cXml punchout response \n%s", + ET.tostring(tree, pretty_print=True), + ) + return False + + encryption_key = backend.buyer_cookie_encryption_key.encode() + fernet = Fernet(encryption_key) + encrypted_buyer_cookie_id_b = encrypted_buyer_cookie_id.encode() + + try: + buyer_cookie_id = fernet.decrypt(encrypted_buyer_cookie_id_b).decode() + except InvalidToken: + _logger.error( + "Unable to decode given buyer cookie. %s", encrypted_buyer_cookie_id + ) + return False + + request = self.sudo().search( + [("buyer_cookie_id", "=", buyer_cookie_id)], limit=1, + ) + if not request: + _logger.error( + "Unable to find a request with given buyer cookie %s", buyer_cookie_id + ) + return False + request.with_user(request.user_id).sudo().write( + { + "cxml_response": ET.tostring(tree, pretty_print=True), + "cxml_response_date": fields.Datetime.now(), + "state": "to_process", + } + ) + request._notify_punchout_response_received() + return request + + def _notify_punchout_response_received(self): + self.ensure_one() + user = self.user_id + user.with_user(user).notify_info( + title=_("Processing the request"), + message=_("The response has been received and will be processed."), + sticky=False, + ) + + def _check_action_process_allowed(self): + for rec in self: + if not rec.action_process_allowed: + raise UserError( + _( + f"You are not allowed to process this request. " + f"{rec.display_name}" + ) + ) + + def action_process(self): + self.ensure_one() + self._check_action_process_allowed() + self.sudo().write({"state": "done", "error_message": False}) + return True diff --git a/punchout/readme/CONFIGURE.md b/punchout/readme/CONFIGURE.md new file mode 100644 index 0000000000..96d3e88fa0 --- /dev/null +++ b/punchout/readme/CONFIGURE.md @@ -0,0 +1,5 @@ +[ This file is optional, it should explain how to configure the module before using it; +it is aimed at advanced users. ] + +To configure this module, you need to access the "PunchOut Backend" menu and create a +new record. diff --git a/punchout/readme/CONTEXT.md b/punchout/readme/CONTEXT.md new file mode 100644 index 0000000000..c296240ddb --- /dev/null +++ b/punchout/readme/CONTEXT.md @@ -0,0 +1,14 @@ +[ This file is optional but strongly suggested to allow end-users to evaluate the +module's usefulness in their context. ] + +This module set up the base of the connection to a punchout system. Sub module can be +created for example to create a purchase order from a shopping cart created in the +punchout platform. To do this, you can make your model inherit from "punchout.request" +and add the import process by inheriting "action_process" method. + +Linked modules: + +- punchout_environment : allows to configure a punchout backend through server + environment files +- punchout_queue_job : link the punchout request to queue jobs, automatically call the + process method when a response is received diff --git a/punchout/readme/CONTRIBUTORS.md b/punchout/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..a38863df35 --- /dev/null +++ b/punchout/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Thomas Binsfeld (ACSONE SA/NV) +- Benjamin Willig (ACSONE SA/NV) diff --git a/punchout/readme/CREDITS.md b/punchout/readme/CREDITS.md new file mode 100644 index 0000000000..c960753568 --- /dev/null +++ b/punchout/readme/CREDITS.md @@ -0,0 +1,6 @@ +[ This file is optional and contains additional credits, other than authors, +contributors, and maintainers. ] + +The development of this module has been financially supported by: + +- ACSONE SA/NV diff --git a/punchout/readme/DESCRIPTION.md b/punchout/readme/DESCRIPTION.md new file mode 100644 index 0000000000..92d76f4e72 --- /dev/null +++ b/punchout/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module setup the base configuration to access a punchout platform, in order to +populate odoo. For example, creating purchase orders. This alpha version has been +implemented using the eShop Wurth. diff --git a/punchout/readme/HISTORY.md b/punchout/readme/HISTORY.md new file mode 100644 index 0000000000..67f1a2eedf --- /dev/null +++ b/punchout/readme/HISTORY.md @@ -0,0 +1,3 @@ +## 13.0.1.0.0 (2023-09-26) + +- [ADD] First version. diff --git a/punchout/readme/INSTALL.md b/punchout/readme/INSTALL.md new file mode 100644 index 0000000000..76fe816a45 --- /dev/null +++ b/punchout/readme/INSTALL.md @@ -0,0 +1,2 @@ +[ This file must only be present if there are very specific installation instructions, +such as installing non-python dependencies. The audience is systems administrators. ] diff --git a/punchout/readme/ROADMAP.md b/punchout/readme/ROADMAP.md new file mode 100644 index 0000000000..67208a8eb8 --- /dev/null +++ b/punchout/readme/ROADMAP.md @@ -0,0 +1,5 @@ +[ Enumerate known caveats and future potential improvements. It is mostly intended for +end-users, and can also help potential new contributors discovering new features to +implement. ] + +- diff --git a/punchout/readme/USAGE.md b/punchout/readme/USAGE.md new file mode 100644 index 0000000000..7f361b5405 --- /dev/null +++ b/punchout/readme/USAGE.md @@ -0,0 +1,13 @@ +[ This file must be present and contains the usage instructions for end-users. As all +other rst files included in the README, it MUST NOT contain reStructuredText sections +only body text (paragraphs, lists, tables, etc). Should you need a more elaborate +structure to explain the addon, please create a Sphinx documentation (which may include +this file as a "quick start" section). ] + +To use this module, you need to: + +1. Configure a punchout backend +2. Click the "Access button". It will create a punchout request. +3. Use the punchout backend website and trigger a response to Odoo +4. Check that the response has been correctly linked to the previously created punchout + request diff --git a/punchout/security/punchout_backend.xml b/punchout/security/punchout_backend.xml new file mode 100644 index 0000000000..52a54479c4 --- /dev/null +++ b/punchout/security/punchout_backend.xml @@ -0,0 +1,23 @@ + + + + + punchout.backend access user + + + + + + + + + punchout.backend access manager + + + + + + + + diff --git a/punchout/security/punchout_request.xml b/punchout/security/punchout_request.xml new file mode 100644 index 0000000000..2f54ce01ff --- /dev/null +++ b/punchout/security/punchout_request.xml @@ -0,0 +1,14 @@ + + + + + punchout.request access manager + + + + + + + + diff --git a/punchout/static/description/icon.png b/punchout/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/punchout/static/description/icon.png differ diff --git a/punchout/tests/__init__.py b/punchout/tests/__init__.py new file mode 100644 index 0000000000..0a2fb1e110 --- /dev/null +++ b/punchout/tests/__init__.py @@ -0,0 +1 @@ +from . import test_punchout diff --git a/punchout/tests/common.py b/punchout/tests/common.py new file mode 100644 index 0000000000..9270338372 --- /dev/null +++ b/punchout/tests/common.py @@ -0,0 +1,44 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests.common import SavepointCase + + +class TestPunchoutCommon(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.backend_model = cls.env["punchout.backend"] + cls.request_model = cls.env["punchout.request"] + cls.buyer_cookie_encryption_key = "9Ndn3znJUntZpwF51nXsMokf1Xt0X3jjolMX-AD5_W0=" + cls.backend = cls.backend_model.create( + { + "name": "/", + "from_domain": "from", + "from_identity": "from", + "to_domain": "to", + "to_identity": "to", + "url": "/", + "shared_secret": "/", + "user_agent": "/", + "deployment_mode": "test", + "browser_form_post_url": "/punchout/cxml/receive/", + "buyer_cookie_encryption_key": cls.buyer_cookie_encryption_key, + } + ) + + cls.request = cls.request_model.create( + { + "backend_id": cls.backend.id, + "buyer_cookie_id": "2-cc162436-fcab-4cfb-888d-abfd8708520d", + } + ) + + def _get_response_xml_content(self, filepath, filename): + filepath = f"{filepath}/cxml/{filename}" + with open(filepath, "rb") as file: + content = file.read() + return content.decode() + + def _store_response(self, cxml_string): + return self.request_model._store_punchout_request(self.backend.id, cxml_string,) diff --git a/punchout/tests/cxml/test_store_request.xml b/punchout/tests/cxml/test_store_request.xml new file mode 100644 index 0000000000..e06b9428f6 --- /dev/null +++ b/punchout/tests/cxml/test_store_request.xml @@ -0,0 +1,133 @@ + + + +
+ + + From + + + + + To + + + + + admin@identity + + Enfinity Suite 6 + +
+ + + gAAAAABlEZQA3rzR0dp8XxpC0cAf7zlPZmFavA9hfy67iNkl5KBPSvWjIVlxL1rSnYIdDg_qF5txVul5j5bTISSQjC0l66lxtPVy1VSKrnbAXL6-GwKYZBmHMVx85kNBrp7XmmRmeFZk + + + 166.3900 + + +
+ contacter + + + Street 1 + City + + 1000 + BE + +
+
+ + 0.0000 + + + + 34.9400 + Value-added tax + +
+ + + 061396325 060 1 + UtQKD90k4GgAAAGKhjMtqsuG@10 + + + + + 37.5300 + + Jeu de tournevis VDE, + plats/cruciformes PH 5 piècesJEU DE + TOURNEVIS-VDE-PH/PLAT-5PIECES + + EA + + + + 7.8813 + Value-added tax + + + + + + + + + + 0890108715078 1 + UtQKD90k4GgAAAGKhjMtqsuG@10 + + + + + 34.0800 + + Nettoyant + freinsBIDON NETTOYANT FREINS 5 L + + EA + + + + 7.1568 + Value-added tax + + + + + + + + + + 0890108720793 1 + UtQKD90k4GgAAAGKhjMtqsuG@10 + + + + + 94.7800 + + Nettoyant + freinsNETTOYANT FREINS REFILLO 20 LITRES + + EA + + + + 19.9038 + Value-added tax + + + + + + + +
+
+
diff --git a/punchout/tests/cxml/test_unknown_buyer_cookie_id.xml b/punchout/tests/cxml/test_unknown_buyer_cookie_id.xml new file mode 100644 index 0000000000..4467595e75 --- /dev/null +++ b/punchout/tests/cxml/test_unknown_buyer_cookie_id.xml @@ -0,0 +1,134 @@ + + + +
+ + + From + + + + + To + + + + + admin@identity + + Enfinity Suite 6 + +
+ + + + gAAAAABlEZ7rysdjEwnI_1kDvjugV9aZ7FUV1Dm8gk4o6mtFd-2I0he19jpvi1uURwzXfDASK_TFpnBf4DJoCHKhGz2m_pUgoTnUlH2iPq9BbrYtRn7sVUNuoZvDKvwLb9mxFQgNaE8Y + + + + 166.3900 + + +
+ contacter + + + Street 1 + City + + 1000 + BE + +
+
+ + 0.0000 + + + + 34.9400 + Value-added tax + +
+ + + 061396325 060 1 + UtQKD90k4GgAAAGKhjMtqsuG@10 + + + + + 37.5300 + + Jeu de tournevis VDE, + plats/cruciformes PH 5 piècesJEU DE + TOURNEVIS-VDE-PH/PLAT-5PIECES + + EA + + + + 7.8813 + Value-added tax + + + + + + + + + + 0890108715078 1 + UtQKD90k4GgAAAGKhjMtqsuG@10 + + + + + 34.0800 + + Nettoyant + freinsBIDON NETTOYANT FREINS 5 L + + EA + + + + 7.1568 + Value-added tax + + + + + + + + + + 0890108720793 1 + UtQKD90k4GgAAAGKhjMtqsuG@10 + + + + + 94.7800 + + Nettoyant + freinsNETTOYANT FREINS REFILLO 20 LITRES + + EA + + + + 19.9038 + Value-added tax + + + + + + + +
+
+
diff --git a/punchout/tests/test_punchout.py b/punchout/tests/test_punchout.py new file mode 100644 index 0000000000..cf77a9d6d8 --- /dev/null +++ b/punchout/tests/test_punchout.py @@ -0,0 +1,60 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import os + +from odoo.tools import mute_logger + +from .common import TestPunchoutCommon + +PATH = os.path.dirname(os.path.abspath(__file__)) + + +class TestPunchout(TestPunchoutCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.backend_model = cls.env["punchout.backend"] + cls.request_model = cls.env["punchout.request"] + + cls.backend = cls.backend_model.create( + { + "name": "/", + "from_domain": "from", + "from_identity": "from", + "to_domain": "to", + "to_identity": "to", + "url": "/", + "shared_secret": "/", + "user_agent": "/", + "deployment_mode": "test", + "browser_form_post_url": "/punchout/cxml/receive/", + "buyer_cookie_encryption_key": ( + "9Ndn3znJUntZpwF51nXsMokf1Xt0X3jjolMX-AD5_W0=" + ), + } + ) + + cls.request = cls.request_model.create( + { + "backend_id": cls.backend.id, + "buyer_cookie_id": "2-cc162436-fcab-4cfb-888d-abfd8708520d", + } + ) + + def test_store_request(self): + request = self.request + cxml_string = self._get_response_xml_content(PATH, "test_store_request.xml") + self._store_response(cxml_string) + self.assertEqual(request.state, "to_process") + self.assertTrue(bool(request.cxml_response)) + + @mute_logger("odoo.addons.punchout.models.punchout_request") + def test_unknown_buyer_cookie_id(self): + request = self.request + cxml_string = self._get_response_xml_content( + PATH, "test_unknown_buyer_cookie_id.xml" + ) + self._store_response(cxml_string) + self.assertEqual(request.state, "draft") + self.assertFalse(bool(request.cxml_response)) diff --git a/punchout/views/punchout_backend.xml b/punchout/views/punchout_backend.xml new file mode 100644 index 0000000000..367bb9bd53 --- /dev/null +++ b/punchout/views/punchout_backend.xml @@ -0,0 +1,99 @@ + + + + + + punchout.backend + +
+
+
+ +
+
+
+ + + + + + + + + + + + + + +
+ + + + + + punchout.backend + + + + + + + + + + + + + + + + punchout.backend + + + + + + + + + + + + + + Punchout Backends + punchout.backend + tree,form + [] + {} + + + + PunchOut + + + + + PunchOut Backends + + + + + diff --git a/punchout/views/punchout_request.xml b/punchout/views/punchout_request.xml new file mode 100644 index 0000000000..877acaa903 --- /dev/null +++ b/punchout/views/punchout_request.xml @@ -0,0 +1,104 @@ + + + + + punchout.request.form (in punchout) + punchout.request + +
+
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + punchout.request.search (in punchout) + punchout.request + + + + + + + + + punchout.request.tree (in punchout) + punchout.request + + + + + + + + + + + PunchOut Requests + punchout.request + tree,form + [] + {} + + + PunchOut Requests + + + + + diff --git a/punchout_environment/__init__.py b/punchout_environment/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/punchout_environment/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/punchout_environment/__manifest__.py b/punchout_environment/__manifest__.py new file mode 100644 index 0000000000..2e855b1d23 --- /dev/null +++ b/punchout_environment/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Punchout Environment", + "version": "13.0.1.0.0", + "license": "AGPL-3", + "author": "Odoo Community Association (OCA), giACSONE SA/NV", + "website": "https://github.com/OCA/edi", + "depends": [ + # oca/edi + "punchout", + # oca/server-env + "server_environment", + ], + "data": [], +} diff --git a/punchout_environment/models/__init__.py b/punchout_environment/models/__init__.py new file mode 100644 index 0000000000..2f98c332e9 --- /dev/null +++ b/punchout_environment/models/__init__.py @@ -0,0 +1 @@ +from . import punchout_backend diff --git a/punchout_environment/models/punchout_backend.py b/punchout_environment/models/punchout_backend.py new file mode 100644 index 0000000000..6743b8d7f0 --- /dev/null +++ b/punchout_environment/models/punchout_backend.py @@ -0,0 +1,24 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class PunchoutBackend(models.Model): + _name = "punchout.backend" + _inherit = ["punchout.backend", "server.env.mixin"] + + @property + def _server_env_fields(self): + return { + "url": {}, + "from_domain": {}, + "from_identity": {}, + "to_domain": {}, + "to_identity": {}, + "shared_secret": {}, + "user_agent": {}, + "deployment_mode": {}, + "browser_form_post_url": {}, + "buyer_cookie_encryption_key": {}, + } diff --git a/punchout_environment/readme/CONTRIBUTORS.md b/punchout_environment/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..a38863df35 --- /dev/null +++ b/punchout_environment/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Thomas Binsfeld (ACSONE SA/NV) +- Benjamin Willig (ACSONE SA/NV) diff --git a/punchout_environment/readme/CREDITS.md b/punchout_environment/readme/CREDITS.md new file mode 100644 index 0000000000..c960753568 --- /dev/null +++ b/punchout_environment/readme/CREDITS.md @@ -0,0 +1,6 @@ +[ This file is optional and contains additional credits, other than authors, +contributors, and maintainers. ] + +The development of this module has been financially supported by: + +- ACSONE SA/NV diff --git a/punchout_environment/readme/DESCRIPTION.md b/punchout_environment/readme/DESCRIPTION.md new file mode 100644 index 0000000000..c8d082904e --- /dev/null +++ b/punchout_environment/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module allows to configure a punchout backend using server environment module. diff --git a/punchout_environment/readme/HISTORY.md b/punchout_environment/readme/HISTORY.md new file mode 100644 index 0000000000..67f1a2eedf --- /dev/null +++ b/punchout_environment/readme/HISTORY.md @@ -0,0 +1,3 @@ +## 13.0.1.0.0 (2023-09-26) + +- [ADD] First version. diff --git a/punchout_environment/readme/USAGE.md b/punchout_environment/readme/USAGE.md new file mode 100644 index 0000000000..5a68d7c80c --- /dev/null +++ b/punchout_environment/readme/USAGE.md @@ -0,0 +1,10 @@ +[ This file must be present and contains the usage instructions for end-users. As all +other rst files included in the README, it MUST NOT contain reStructuredText sections +only body text (paragraphs, lists, tables, etc). Should you need a more elaborate +structure to explain the addon, please create a Sphinx documentation (which may include +this file as a "quick start" section). ] + +To use this module, you need to: + +1. Trigger the "Process" action on a request +2. Check the linked jobs using the smart button available on the punchout request diff --git a/punchout_environment/static/description/icon.png b/punchout_environment/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/punchout_environment/static/description/icon.png differ diff --git a/punchout_queue_job/__init__.py b/punchout_queue_job/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/punchout_queue_job/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/punchout_queue_job/__manifest__.py b/punchout_queue_job/__manifest__.py new file mode 100644 index 0000000000..e53e884944 --- /dev/null +++ b/punchout_queue_job/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Punchout Queue Job", + "version": "13.0.1.0.0", + "license": "AGPL-3", + "author": "Odoo Community Association (OCA), ACSONE SA/NV", + "website": "https://github.com/OCA/edi", + "depends": [ + # oca/connector + "queue_job", + # oca/edi + "punchout", + ], + "data": ["views/punchout_request.xml"], + "demo": [], +} diff --git a/punchout_queue_job/i18n/fr.po b/punchout_queue_job/i18n/fr.po new file mode 100644 index 0000000000..59a389584c --- /dev/null +++ b/punchout_queue_job/i18n/fr.po @@ -0,0 +1,36 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * punchout_queue_job +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-26 13:27+0000\n" +"PO-Revision-Date: 2023-09-26 13:27+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: punchout_queue_job +#: model:ir.model.fields,field_description:punchout_queue_job.field_punchout_request__has_job_access +msgid "Has Job Access" +msgstr "" + +#. module: punchout_queue_job +#: model:ir.model.fields,field_description:punchout_queue_job.field_punchout_request__job_count +msgid "Job Count" +msgstr "Nombre de tâches" + +#. module: punchout_queue_job +#: model_terms:ir.ui.view,arch_db:punchout_queue_job.punchout_request_form_view +msgid "Jobs" +msgstr "Tâches" + +#. module: punchout_queue_job +#: model:ir.model,name:punchout_queue_job.model_punchout_request +msgid "Punchout Request" +msgstr "Requête Punchout " \ No newline at end of file diff --git a/punchout_queue_job/models/__init__.py b/punchout_queue_job/models/__init__.py new file mode 100644 index 0000000000..733a40f2ae --- /dev/null +++ b/punchout_queue_job/models/__init__.py @@ -0,0 +1 @@ +from . import punchout_request diff --git a/punchout_queue_job/models/punchout_request.py b/punchout_queue_job/models/punchout_request.py new file mode 100644 index 0000000000..9e61832f70 --- /dev/null +++ b/punchout_queue_job/models/punchout_request.py @@ -0,0 +1,57 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, fields, models +from odoo.exceptions import AccessError + +_logger = logging.getLogger(__name__) + + +class PunchoutRequest(models.Model): + _inherit = "punchout.request" + + has_job_access = fields.Boolean(compute="_compute_has_job_access",) + job_count = fields.Integer(compute="_compute_job_count",) + + @api.depends_context("uid") + def _compute_has_job_access(self): + try: + self.env["queue.job"].check_access_rights("read") + self.has_job_access = True + except AccessError: + self.has_job_access = False + + def _compute_job_count(self): + job_model = self.env["queue.job"] + for rec in self: + rec.job_count = ( + job_model.search_count(rec._get_queue_job_list_domain()) + if rec.has_job_access + else 0 + ) + + def _get_queue_job_description(self): + self.ensure_one() + return f"Import of Purchase Order through Punchout request {self.display_name}" + + def _get_queue_job_list_domain(self): + self.ensure_one() + return [ + ("name", "=", self._get_queue_job_description()), + ] + + def action_show_job_list(self): + self.ensure_one() + action = self.env.ref("queue_job.action_queue_job").read([])[0] + action.update({"domain": self._get_queue_job_list_domain(), "context": {}}) + return action + + def _store_punchout_request(self, *args, **kwargs): + request = super()._store_punchout_request(*args, **kwargs) + if request: + request.with_user(request.user_id).with_delay( + description=request._get_queue_job_description() + ).action_process() + return request diff --git a/punchout_queue_job/readme/CONTEXT.md b/punchout_queue_job/readme/CONTEXT.md new file mode 100644 index 0000000000..6c4fe13d27 --- /dev/null +++ b/punchout_queue_job/readme/CONTEXT.md @@ -0,0 +1,4 @@ +[ This file is optional but strongly suggested to allow end-users to evaluate the +module's usefulness in their context. ] + +Run the process of the punchout response through a job after it has been received. diff --git a/punchout_queue_job/readme/CONTRIBUTORS.md b/punchout_queue_job/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..a38863df35 --- /dev/null +++ b/punchout_queue_job/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Thomas Binsfeld (ACSONE SA/NV) +- Benjamin Willig (ACSONE SA/NV) diff --git a/punchout_queue_job/readme/CREDITS.md b/punchout_queue_job/readme/CREDITS.md new file mode 100644 index 0000000000..c960753568 --- /dev/null +++ b/punchout_queue_job/readme/CREDITS.md @@ -0,0 +1,6 @@ +[ This file is optional and contains additional credits, other than authors, +contributors, and maintainers. ] + +The development of this module has been financially supported by: + +- ACSONE SA/NV diff --git a/punchout_queue_job/readme/DESCRIPTION.md b/punchout_queue_job/readme/DESCRIPTION.md new file mode 100644 index 0000000000..a937e146ca --- /dev/null +++ b/punchout_queue_job/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Run the process of the punchout response through a job after it has been received. diff --git a/punchout_queue_job/readme/HISTORY.md b/punchout_queue_job/readme/HISTORY.md new file mode 100644 index 0000000000..67f1a2eedf --- /dev/null +++ b/punchout_queue_job/readme/HISTORY.md @@ -0,0 +1,3 @@ +## 13.0.1.0.0 (2023-09-26) + +- [ADD] First version. diff --git a/punchout_queue_job/readme/USAGE.md b/punchout_queue_job/readme/USAGE.md new file mode 100644 index 0000000000..5a68d7c80c --- /dev/null +++ b/punchout_queue_job/readme/USAGE.md @@ -0,0 +1,10 @@ +[ This file must be present and contains the usage instructions for end-users. As all +other rst files included in the README, it MUST NOT contain reStructuredText sections +only body text (paragraphs, lists, tables, etc). Should you need a more elaborate +structure to explain the addon, please create a Sphinx documentation (which may include +this file as a "quick start" section). ] + +To use this module, you need to: + +1. Trigger the "Process" action on a request +2. Check the linked jobs using the smart button available on the punchout request diff --git a/punchout_queue_job/static/description/icon.png b/punchout_queue_job/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/punchout_queue_job/static/description/icon.png differ diff --git a/punchout_queue_job/views/punchout_request.xml b/punchout_queue_job/views/punchout_request.xml new file mode 100644 index 0000000000..9769617441 --- /dev/null +++ b/punchout_queue_job/views/punchout_request.xml @@ -0,0 +1,23 @@ + + + + + punchout.request + + + + + + + + + diff --git a/requirements.txt b/requirements.txt index ea466fcc0d..0cf3e25f66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ # generated from manifests external_dependencies +cryptography factur-x lxml pyyaml diff --git a/setup/punchout/odoo/addons/punchout b/setup/punchout/odoo/addons/punchout new file mode 120000 index 0000000000..731eee884f --- /dev/null +++ b/setup/punchout/odoo/addons/punchout @@ -0,0 +1 @@ +../../../../punchout \ No newline at end of file diff --git a/setup/punchout/setup.py b/setup/punchout/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/punchout/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/punchout_environment/odoo/addons/punchout_environment b/setup/punchout_environment/odoo/addons/punchout_environment new file mode 120000 index 0000000000..db6cb4440f --- /dev/null +++ b/setup/punchout_environment/odoo/addons/punchout_environment @@ -0,0 +1 @@ +../../../../punchout_environment \ No newline at end of file diff --git a/setup/punchout_environment/setup.py b/setup/punchout_environment/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/punchout_environment/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/punchout_queue_job/odoo/addons/punchout_queue_job b/setup/punchout_queue_job/odoo/addons/punchout_queue_job new file mode 120000 index 0000000000..a30a1bb994 --- /dev/null +++ b/setup/punchout_queue_job/odoo/addons/punchout_queue_job @@ -0,0 +1 @@ +../../../../punchout_queue_job \ No newline at end of file diff --git a/setup/punchout_queue_job/setup.py b/setup/punchout_queue_job/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/punchout_queue_job/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)