From 23618c210fa44509b15b6af883f18ab573d8fa70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 23 Dec 2025 12:07:17 +0100 Subject: [PATCH] Add support for Peppyrus webhooks --- modules/edocument_peppol_peppyrus/CHANGELOG | 1 + modules/edocument_peppol_peppyrus/__init__.py | 4 ++ .../doc/configuration.rst | 17 +++++ .../edocument_peppol_peppyrus/doc/index.rst | 2 + .../doc/reference.rst | 23 +++++++ .../edocument_peppol_peppyrus/edocument.py | 63 +++++++++++++++++- .../edocument_peppol_peppyrus/edocument.xml | 7 ++ modules/edocument_peppol_peppyrus/routes.py | 66 +++++++++++++++++++ .../scenario_edocument_peppol_peppyrus.rst | 8 ++- .../view/edocument_peppol_service_form.xml | 7 ++ 10 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 modules/edocument_peppol_peppyrus/doc/configuration.rst create mode 100644 modules/edocument_peppol_peppyrus/doc/reference.rst create mode 100644 modules/edocument_peppol_peppyrus/routes.py diff --git a/modules/edocument_peppol_peppyrus/CHANGELOG b/modules/edocument_peppol_peppyrus/CHANGELOG index ae8fc7ba72..827b6e8ece 100644 --- a/modules/edocument_peppol_peppyrus/CHANGELOG +++ b/modules/edocument_peppol_peppyrus/CHANGELOG @@ -1,3 +1,4 @@ +* Add support for webhooks Version 7.8.0 - 2025-12-15 -------------------------- diff --git a/modules/edocument_peppol_peppyrus/__init__.py b/modules/edocument_peppol_peppyrus/__init__.py index 4effdfa4a2..aed6e0e6c7 100644 --- a/modules/edocument_peppol_peppyrus/__init__.py +++ b/modules/edocument_peppol_peppyrus/__init__.py @@ -1,2 +1,6 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. + +from . import routes + +__all__ = [routes] diff --git a/modules/edocument_peppol_peppyrus/doc/configuration.rst b/modules/edocument_peppol_peppyrus/doc/configuration.rst new file mode 100644 index 0000000000..82695f080b --- /dev/null +++ b/modules/edocument_peppol_peppyrus/doc/configuration.rst @@ -0,0 +1,17 @@ +************* +Configuration +************* + +The *EDocument Peppol Peppyrus Module* uses values from settings in the +``[edocument_peppol_peppyrus]`` section of the +:ref:`trytond:topics-configuration`. + +.. _config-edocument_peppol_peppyrus.max_size: + +``max_size`` +============ + +The maximum size in bytes of the Peppyrus Webhook request (zero means no +limit): + +The default value is: `trytond:config-request.max_size` diff --git a/modules/edocument_peppol_peppyrus/doc/index.rst b/modules/edocument_peppol_peppyrus/doc/index.rst index f64d9f6de3..111fcf95d8 100644 --- a/modules/edocument_peppol_peppyrus/doc/index.rst +++ b/modules/edocument_peppol_peppyrus/doc/index.rst @@ -10,5 +10,7 @@ documents on the `Peppol network `_ thanks to the :maxdepth: 2 setup + configuration design + reference releases diff --git a/modules/edocument_peppol_peppyrus/doc/reference.rst b/modules/edocument_peppol_peppyrus/doc/reference.rst new file mode 100644 index 0000000000..2327abc5d0 --- /dev/null +++ b/modules/edocument_peppol_peppyrus/doc/reference.rst @@ -0,0 +1,23 @@ +************* +API Reference +************* + +.. _Peppyrus Webhook: + +Peppyrus Webhook +================ + +The *EDocument Peppol Peppyrus MOdule* defines routes to receive the Peppyrus' +webhooks: + + - ``POST`` ``//edocument_peppol_peppyrus//in``: + Stores the payload message. + + ``identifier`` is the Peppyrus identifier of the `Service + `. + + - ``POST`` ``//edocument_peppol_peppyrus//out``: + Update the status of the payload message. + + ``identifier`` is the Peppyrus identifier of the `Service + `. diff --git a/modules/edocument_peppol_peppyrus/edocument.py b/modules/edocument_peppol_peppyrus/edocument.py index 06ed328a27..446292704f 100644 --- a/modules/edocument_peppol_peppyrus/edocument.py +++ b/modules/edocument_peppol_peppyrus/edocument.py @@ -1,10 +1,11 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +import uuid from base64 import b64decode, b64encode from functools import wraps from io import BytesIO -from urllib.parse import urljoin +from urllib.parse import quote, urljoin import requests from lxml import etree @@ -16,6 +17,7 @@ from trytond.pool import Pool, PoolMeta from trytond.protocols.wrappers import HTTPStatus from trytond.pyson import Eval from trytond.transaction import Transaction +from trytond.url import http_host from .exceptions import PeppyrusCredentialWarning, PeppyrusError @@ -57,11 +59,32 @@ class PeppolService(metaclass=PoolMeta): 'invisible': Eval('service') != 'peppyrus', 'required': Eval('service') == 'peppyrus', }) + peppyrus_identifier = fields.Char( + "Identifier", readonly=True, + states={ + 'invisible': Eval('service') != 'peppyrus', + }) + peppyrus_incoming_webhook = fields.Function( + fields.Char( + "Incoming webhook", + help="The URL for the incoming documents webhook."), + 'on_change_with_peppyrus_incoming_webhook') + peppyrus_outgoing_webhook = fields.Function( + fields.Char( + "Outgoing webhook", + help="The URL for the outgoing documents webhook."), + 'on_change_with_peppyrus_outgoing_webhook') @classmethod def __setup__(cls): super().__setup__() cls.service.selection.append(('peppyrus', "Peppyrus")) + cls._buttons.update( + peppyrus_new_identifier={ + 'invisible': Eval('service') != 'peppyrus', + 'icon': 'tryton-refresh', + }, + ) @fields.depends('service', 'peppyrus_server') def on_change_service(self): @@ -69,6 +92,41 @@ class PeppolService(metaclass=PoolMeta): and not self.peppyrus_server): self.peppyrus_server = 'testing' + @fields.depends('peppyrus_identifier') + def on_change_with_peppyrus_incoming_webhook(self, name=None): + if self.peppyrus_identifier: + url_part = { + 'identifier': self.peppyrus_identifier, + 'database_name': Transaction().database.name, + } + return http_host() + ( + quote( + '/%(database_name)s/edocument_peppol_peppyrus/' + '%(identifier)s/in' + % url_part)) + + @fields.depends('peppyrus_identifier') + def on_change_with_peppyrus_outgoing_webhook(self, name=None): + if self.peppyrus_identifier: + url_part = { + 'identifier': self.peppyrus_identifier, + 'database_name': Transaction().database.name, + } + return http_host() + ( + quote( + '/%(database_name)s/edocument_peppol_peppyrus/' + '%(identifier)s/out' + % url_part)) + + @classmethod + def peppyrus_new_identifier(cls, services): + for service in services: + if service.peppyrus_identifier: + service.peppyrus_identifier = None + else: + service.peppyrus_identifier = uuid.uuid4().hex + cls.save(services) + @peppyrus_api def _post_peppyrus(self, document): tree = etree.parse(BytesIO(document.data)) @@ -133,6 +191,9 @@ class PeppolService(metaclass=PoolMeta): }) response.raise_for_status() message = response.json() + self.peppyrus_update(document, message) + + def peppyrus_update(self, document, message): assert message['id'] == document.transmission_id if message['folder'] == 'sent': document.succeed() diff --git a/modules/edocument_peppol_peppyrus/edocument.xml b/modules/edocument_peppol_peppyrus/edocument.xml index 15ba1845d3..4b859cd9d8 100644 --- a/modules/edocument_peppol_peppyrus/edocument.xml +++ b/modules/edocument_peppol_peppyrus/edocument.xml @@ -8,6 +8,13 @@ this repository contains the full copyright notices and license terms. --> edocument_peppol_service_form + + + edocument.peppol.service + peppyrus_new_identifier + New URLs + This action will make the previous URLs unusable. Do you want to continue? + diff --git a/modules/edocument_peppol_peppyrus/routes.py b/modules/edocument_peppol_peppyrus/routes.py new file mode 100644 index 0000000000..6ea8c5a236 --- /dev/null +++ b/modules/edocument_peppol_peppyrus/routes.py @@ -0,0 +1,66 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + +import json +import logging + +from trytond import config +from trytond.protocols.wrappers import ( + HTTPStatus, Response, abort, set_max_request_size, with_pool, + with_transaction) +from trytond.wsgi import app + +logger = logging.getLogger(__name__) + + +@app.route( + '//edocument_peppol_peppyrus//in', + methods={'POST'}) +@set_max_request_size(config.getint( + 'edocument_peppol_peppyrus', 'max_size', + default=config.getint('request', 'max_size'))) +@with_pool +@with_transaction() +def incoming(request, pool, identifier): + Service = pool.get('edocument.peppol.service') + + try: + service, = Service.search([ + ('peppyrus_identifier', '=', identifier), + ]) + except ValueError: + abort(HTTPStatus.NOT_FOUND) + + request_body = request.get_data(as_text=True) + message = json.loads(request_body) + Service.peppyrus_store(message) + return Response(status=HTTPStatus.NO_CONTENT) + + +@app.route( + '//edocument_peppol_peppyrus//out', + methods={'POST'}) +@set_max_request_size(config.getint( + 'edocument_peppol_peppyrus', 'max_size', + default=config.getint('request', 'max_size'))) +@with_pool +@with_transaction() +def outgoing(request, pool, identifier): + Service = pool.get('edocument.peppol.service') + Document = pool.get('edocument.peppol') + + try: + service, = Service.search([ + ('peppyrus_identifier', '=', identifier), + ]) + except ValueError: + abort(HTTPStatus.NOT_FOUND) + + request_body = request.get_data(as_text=True) + message = json.loads(request_body) + document, = Document.search([ + ('service', '=', service), + ('transmission_id', '=', message['id']), + ]) + service.peppyrus_update(document, message) + return Response(status=HTTPStatus.NO_CONTENT) diff --git a/modules/edocument_peppol_peppyrus/tests/scenario_edocument_peppol_peppyrus.rst b/modules/edocument_peppol_peppyrus/tests/scenario_edocument_peppol_peppyrus.rst index d082865841..55ddcbc1c0 100644 --- a/modules/edocument_peppol_peppyrus/tests/scenario_edocument_peppol_peppyrus.rst +++ b/modules/edocument_peppol_peppyrus/tests/scenario_edocument_peppol_peppyrus.rst @@ -11,7 +11,7 @@ Imports:: >>> from proteus import Model >>> from trytond.modules.company.tests.tools import create_company >>> from trytond.modules.edocument_peppol.edocument import Peppol - >>> from trytond.tests.tools import activate_modules + >>> from trytond.tests.tools import activate_modules, assertTrue >>> from trytond.tools import file_open >>> FETCH_SLEEP, MAX_SLEEP = 1, 20 @@ -48,6 +48,12 @@ Create a service:: >>> peppol_service.peppyrus_api_key = os.getenv('PEPPYRUS_API_KEY') >>> peppol_service.save() +Setup webhook:: + + >>> peppol_service.click('peppyrus_new_identifier') + >>> assertTrue(peppol_service.peppyrus_incoming_webhook) + >>> assertTrue(peppol_service.peppyrus_outgoing_webhook) + Send out a Peppol document:: >>> peppol = Peppol(direction='out') diff --git a/modules/edocument_peppol_peppyrus/view/edocument_peppol_service_form.xml b/modules/edocument_peppol_peppyrus/view/edocument_peppol_service_form.xml index f8265a448b..96b54c788e 100644 --- a/modules/edocument_peppol_peppyrus/view/edocument_peppol_service_form.xml +++ b/modules/edocument_peppol_peppyrus/view/edocument_peppol_service_form.xml @@ -8,6 +8,13 @@ this repository contains the full copyright notices and license terms. -->