Friday, December 20, 2024¶
Today I continued to work on Ticket #5670 (Support e-invoices using PEPPOL).
I decided to play with the source code of the peppol-py package. Thanks for publishing your work, Anders Rune Jensen!
Is it possible that my customers can (and must) become an “access point” of the Peppol network? So that I just need to install them something like this script? And when they register a sales invoice in TIM, TIM would call this script, which would connect to the Peppol network and say “Hello, here I have a new invoice”.
Before I actually started playing, I worked on the topic page Lino and eInvoicing (PEPPOL), trying to get a more or less satisfying understanding of PEPPOL into my little brain. And I got quickly lost.
For example the first sentence of the README.md file says that it is a “python implementation for sending peppol eDelivery AS4 documents”. So I wanted to know what this AS4 actually means. On docs.peppol.eu I found found the following definition:
In short, AS4 is used in the Peppol network for transmission of asynchronous messages between corner 2 and corner 3 in a Four Corner Topology using the Peppol PKI for signature and encryption on AS4 message level and SMP/SML for dynamic discovery.
Did you understand anything? I didn’t. This is a good example of why I need my own definitions… Surf, surf, okay, everybody except me seems to know what this “AS4” stands for, but let’s not get stuck any longer because of this. Let’s have a look at this source code instead.
The main script sender.py wants at least
two arguments: a --filename
(I guess that this should be the XML file of the
invoice) and a --receiver
(which is then referred to as their_id
). And
what does it do with this receiver?
I copied sml.py
and smp.py into
a file 1220.py
(see below) in order to play with it.
The README gives an example “9922:NGTBCNTRLP1001” for the --receiver
argument, and I guess that 9922 is an EAS code (meaning “Andorra VAT
number”). For my tests I’d rather use 9931 (Estonia VAT number) and our own VAT
number, which is EE100588749.
Here is my playground script 1220.py
:
import hashlib
from lxml import etree
import urllib.request
import urllib.parse
# copied from sml.py:
# SML: receiver -> domain (DNS)
def get_domain_using_http(receiver, test):
smp_id = 'B-' + hashlib.md5((receiver.lower()).encode("utf-8")).hexdigest()
return f'{smp_id}.iso6523-actorid-upis.{get_server(test)}'
def get_server(test):
if test:
return 'acc.edelivery.tech.ec.europa.eu'
else:
return 'edelivery.tech.ec.europa.eu'
# copied from smp.py:
# SMP: domain + path -> xml with service descriptions
def get_smp_info(domain, receiver):
# all the available interfaces (invoice, credit note etc.)
url = 'http://' + domain + "/iso6523-actorid-upis::" + receiver
print(f"request from url {url}")
contents = urllib.request.urlopen(url).read()
print(contents)
return contents
invoice_end = urllib.parse.quote("billing:3.0::2.1")
def find_invoice_smp_document(smp_contents):
root = etree.fromstring(smp_contents)
for child in root:
for el in child:
if el.get('href').endswith(invoice_end):
return el.get('href')
def extract_as4_information(smp_contents):
invoice_url = find_invoice_smp_document(smp_contents)
invoice_smp = urllib.request.urlopen(invoice_url).read()
root = etree.fromstring(invoice_smp)
#id = root.findall(".//{*}ParticipantIdentifier")[0].text
as4_endpoint = root.findall(".//{*}EndpointReference")[0][0].text
certificate = root.findall(".//{*}Certificate")[0].text
return [as4_endpoint, certificate]
their_id = "9931:EE100588749" # Rumma & Ko OÜ
test = True
smp_domain = get_domain_using_http(their_id, test)
print(f"smp_domain is {smp_domain}")
smp_contents = get_smp_info(smp_domain, their_id)
url, their_cert = extract_as4_information(smp_contents)
print(url, their_cert)
#their_certfile = '/tmp/their-cert.pem'
#with open(their_certfile, 'w') as f:
# f.write('-----BEGIN CERTIFICATE-----\n' + their_cert + '\n-----END CERTIFICATE-----')
Here is the output of my script:
smp_domain is B-e0aa140770a8690c4a3512bc5cdfaeb9.iso6523-actorid-upis.acc.edelivery.tech.ec.europa.eu
request from url http://B-e0aa140770a8690c4a3512bc5cdfaeb9.iso6523-actorid-upis.acc.edelivery.tech.ec.europa.eu/iso6523-actorid-upis::9931:EE100588749
Traceback (most recent call last):
...
socket.gaierror: [Errno -2] Name or service not known
So basically it builds a URL that requests from
acc.edelivery.tech.ec.europa.eu
but gets an error “Name or service not
known” as response.
That server (acc.edelivery.tech.ec.europa.eu) seems to exist (at least it has an
IP address) but doesn’t answer to ping
:
$ ping acc.edelivery.tech.ec.europa.eu
PING acc.edelivery.tech.ec.europa.eu (147.67.35.45) 56(84) bytes of data.
^C
--- acc.edelivery.tech.ec.europa.eu ping statistics ---
109 packets transmitted, 0 received, 100% packet loss, time 110629ms
I must be missing something.
I guess that the --receiver
is not my customer (the receiver of the invoice)
but a certified “Service Metadata Publisher” (SMP). To become an SMP
they need to join OpenPeppol AISBL.
That’s definitively not something my customers will do.
So these SMPs or APs are certified by Beppol authorities. Every country has a PEPPOL authority. For Belgium it is BOSA: https://bosa.belgium.be
And now I see that Hermes is back! I thought that it had vanished!
https://bosa.belgium.be/fr/applications/hermes
And my customers in Belgium can probably sign in to Hermes:
https://hermes-belgium.be/hermeslogin?lang=fr
Next step then is to explore Hermes with my customers and find out how Lino can upload their XML invoices to it.