details: https://code.tryton.org/tryton/commit/b96191427ff3
branch: default
user: Cédric Krier <[email protected]>
date: Tue Dec 23 12:07:17 2025 +0100
description:
Add support for Peppyrus webhooks
diffstat:
modules/edocument_peppol_peppyrus/CHANGELOG
| 1 +
modules/edocument_peppol_peppyrus/__init__.py
| 4 +
modules/edocument_peppol_peppyrus/doc/configuration.rst
| 17 ++
modules/edocument_peppol_peppyrus/doc/index.rst
| 2 +
modules/edocument_peppol_peppyrus/doc/reference.rst
| 23 +++
modules/edocument_peppol_peppyrus/edocument.py
| 63 +++++++++-
modules/edocument_peppol_peppyrus/edocument.xml
| 7 +
modules/edocument_peppol_peppyrus/routes.py
| 66 ++++++++++
modules/edocument_peppol_peppyrus/tests/scenario_edocument_peppol_peppyrus.rst
| 8 +-
modules/edocument_peppol_peppyrus/view/edocument_peppol_service_form.xml
| 7 +
10 files changed, 196 insertions(+), 2 deletions(-)
diffs (315 lines):
diff -r f2293673b1b3 -r b96191427ff3 modules/edocument_peppol_peppyrus/CHANGELOG
--- a/modules/edocument_peppol_peppyrus/CHANGELOG Thu Dec 18 15:18:53
2025 +0100
+++ b/modules/edocument_peppol_peppyrus/CHANGELOG Tue Dec 23 12:07:17
2025 +0100
@@ -1,3 +1,4 @@
+* Add support for webhooks
Version 7.8.0 - 2025-12-15
--------------------------
diff -r f2293673b1b3 -r b96191427ff3
modules/edocument_peppol_peppyrus/__init__.py
--- a/modules/edocument_peppol_peppyrus/__init__.py Thu Dec 18 15:18:53
2025 +0100
+++ b/modules/edocument_peppol_peppyrus/__init__.py Tue Dec 23 12:07:17
2025 +0100
@@ -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 -r f2293673b1b3 -r b96191427ff3
modules/edocument_peppol_peppyrus/doc/configuration.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_peppol_peppyrus/doc/configuration.rst Tue Dec 23
12:07:17 2025 +0100
@@ -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 -r f2293673b1b3 -r b96191427ff3
modules/edocument_peppol_peppyrus/doc/index.rst
--- a/modules/edocument_peppol_peppyrus/doc/index.rst Thu Dec 18 15:18:53
2025 +0100
+++ b/modules/edocument_peppol_peppyrus/doc/index.rst Tue Dec 23 12:07:17
2025 +0100
@@ -10,5 +10,7 @@
:maxdepth: 2
setup
+ configuration
design
+ reference
releases
diff -r f2293673b1b3 -r b96191427ff3
modules/edocument_peppol_peppyrus/doc/reference.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_peppol_peppyrus/doc/reference.rst Tue Dec 23
12:07:17 2025 +0100
@@ -0,0 +1,23 @@
+*************
+API Reference
+*************
+
+.. _Peppyrus Webhook:
+
+Peppyrus Webhook
+================
+
+The *EDocument Peppol Peppyrus MOdule* defines routes to receive the Peppyrus'
+webhooks:
+
+ - ``POST`` ``/<database_name>/edocument_peppol_peppyrus/<identifier>/in``:
+ Stores the payload message.
+
+ ``identifier`` is the Peppyrus identifier of the `Service
+ <model-edocument.peppol.service>`.
+
+ - ``POST`` ``/<database_name>/edocument_peppol_peppyrus/<identifier>/out``:
+ Update the status of the payload message.
+
+ ``identifier`` is the Peppyrus identifier of the `Service
+ <model-edocument.peppol.service>`.
diff -r f2293673b1b3 -r b96191427ff3
modules/edocument_peppol_peppyrus/edocument.py
--- a/modules/edocument_peppol_peppyrus/edocument.py Thu Dec 18 15:18:53
2025 +0100
+++ b/modules/edocument_peppol_peppyrus/edocument.py Tue Dec 23 12:07:17
2025 +0100
@@ -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.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 @@
'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 @@
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 @@
})
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 -r f2293673b1b3 -r b96191427ff3
modules/edocument_peppol_peppyrus/edocument.xml
--- a/modules/edocument_peppol_peppyrus/edocument.xml Thu Dec 18 15:18:53
2025 +0100
+++ b/modules/edocument_peppol_peppyrus/edocument.xml Tue Dec 23 12:07:17
2025 +0100
@@ -8,6 +8,13 @@
<field name="inherit"
ref="edocument_peppol.edocument_peppol_service_view_form"/>
<field name="name">edocument_peppol_service_form</field>
</record>
+
+ <record model="ir.model.button"
id="edocument_peppol_service_new_identifier_button">
+ <field name="model">edocument.peppol.service</field>
+ <field name="name">peppyrus_new_identifier</field>
+ <field name="string">New URLs</field>
+ <field name="confirm">This action will make the previous URLs
unusable. Do you want to continue?</field>
+ </record>
</data>
<data noupdate="1">
diff -r f2293673b1b3 -r b96191427ff3 modules/edocument_peppol_peppyrus/routes.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_peppol_peppyrus/routes.py Tue Dec 23 12:07:17
2025 +0100
@@ -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__)
+
+
[email protected](
+ '/<database_name>/edocument_peppol_peppyrus/<identifier>/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)
+
+
[email protected](
+ '/<database_name>/edocument_peppol_peppyrus/<identifier>/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 -r f2293673b1b3 -r b96191427ff3
modules/edocument_peppol_peppyrus/tests/scenario_edocument_peppol_peppyrus.rst
---
a/modules/edocument_peppol_peppyrus/tests/scenario_edocument_peppol_peppyrus.rst
Thu Dec 18 15:18:53 2025 +0100
+++
b/modules/edocument_peppol_peppyrus/tests/scenario_edocument_peppol_peppyrus.rst
Tue Dec 23 12:07:17 2025 +0100
@@ -11,7 +11,7 @@
>>> 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 @@
>>> 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 -r f2293673b1b3 -r b96191427ff3
modules/edocument_peppol_peppyrus/view/edocument_peppol_service_form.xml
--- a/modules/edocument_peppol_peppyrus/view/edocument_peppol_service_form.xml
Thu Dec 18 15:18:53 2025 +0100
+++ b/modules/edocument_peppol_peppyrus/view/edocument_peppol_service_form.xml
Tue Dec 23 12:07:17 2025 +0100
@@ -8,6 +8,13 @@
<field name="peppyrus_server"/>
<label name="peppyrus_api_key"/>
<field name="peppyrus_api_key"/>
+
+ <label name="peppyrus_incoming_webhook"/>
+ <field name="peppyrus_incoming_webhook"/>
+ <label name="peppyrus_outgoing_webhook"/>
+ <field name="peppyrus_outgoing_webhook"/>
+
+ <button name="peppyrus_new_identifier" colspan="4"/>
<newline/>
</xpath>
</data>