Add support for Peppyrus webhooks

This commit is contained in:
Cédric Krier
2025-12-23 12:07:17 +01:00
parent aecb2942fe
commit 23618c210f
10 changed files with 196 additions and 2 deletions
@@ -1,3 +1,4 @@
* Add support for webhooks
Version 7.8.0 - 2025-12-15
--------------------------
@@ -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]
@@ -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`
@@ -10,5 +10,7 @@ documents on the `Peppol network <https://peppol.org/>`_ thanks to the
:maxdepth: 2
setup
configuration
design
reference
releases
@@ -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>`.
+62 -1
View File
@@ -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()
@@ -8,6 +8,13 @@ this repository contains the full copyright notices and license terms. -->
<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">
@@ -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(
'/<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)
@app.route(
'/<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)
@@ -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')
@@ -8,6 +8,13 @@ this repository contains the full copyright notices and license terms. -->
<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>