Add support for Peppyrus webhooks
This commit is contained in:
@@ -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>`.
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user