Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

ibanity : Peppol access via Ibanity

The lino_xl.lib.ibanity plugin adds functionality for accessing the Peppol network using the Flowin e-invoicing API by Ibanity. The plugin is under development, contact us to get latest information on the progress.

For general introduction to Peppol, see Lino and Peppol (eInvoicing).

Note

Code snippets in this document (lines starting with >>>) get tested as part of our development workflow. The following initialization snippet tells you which demo project is being used.

>>> from lino_book.projects.noi1r.startup import *

The tests in this document are skipped unless you have Ibanity credentials installed. See How to set up your credentials for details.

>>> if dd.plugins.ibanity.credentials is None:
...     pytest.skip('this doctest requires Ibanity credentials')

Usage scenarios

The ibanity plugin has two usage scenarios called “supplier management” and “document management”. Both scenarios can be combined on a single Lino site : a Lino hosting provider who is a customer of Ibanity can use their certificate also for their own accounting.

This document shows things that are used in both usage scenarios. For more specific documentation about each usage scenario, see ibanity in Noi and ibanity (Ibanity in Così).

Ibanity jargon

Ibanity

A Peppol Access Point provider for software developers who access the Peppol network via an API.

See https://ibanity.com/company

Ibanity is a solution of Isabel Group in Brussels.

Ibanity end user

An organization that receives and sends their invoices and credit notes via a software that uses the Ibanity API to access the Peppol network.

Ibanity supplier

Jargon synonym for Ibanity end user used by the Ibanity API for historical reasons.

Ibanity client

An organization that is a certified Ibanity customer and who can register their own customers as suppliers.

Ibanity API

The public Application Programmers Interface provided by Ibanity to their clients.

Ibanity developer portal

The web interface where an Ibanity client gets their credentials.

See https://documentation.ibanity.com/go-live

Ibanity credentials

A set of files with security keys to identify an Ibanity client when accessing the Ibanity API.

How to set up your credentials

If you want to play yourself with the Ibanity API, or if you want to configure a production site, you need to set up your credential files so that Lino can access the Ibanity API. Here is how to do this.

  • We assume that you have your Lino developer environment installed. Go to your copy of the cosi1 demo project and create a subdirectory named secrets:

    $ go cosi1
    $ mkdir secrets
    

    Note that directories named secrets are ignored by Git because they are listed in the .gitignore file.

  • Create an account on the Ibanity developer portal, create a sandbox application, activate the “Flowin e-invoicing” product, generate a certificate, extract the certificate files and store them into the secrets subdirectory. You also need to decrypt the private key because Python’s requests module doesn’t support encrypted keys:

    $ go cosi1
    $ cd secrets
    $ openssl rsa -in private_key.pem -out decrypted_private_key.pem
    
  • Still in the secrets directory, create a file named credentials.txt that contains client_id and your client_secret. The file must contain a single line of text in the format {client_id}:{client_secret}.

Now you should be able to test this document by saying:

$ go book
$ doctest docs/plugins/ibanity.rst

Exploring the Ibanity API

This section explores the Ibanity API, also known as the the Flowin e-invoicing Services.

Access token

The ibanity.credentials setting is the identifier of your application in the Ibanity developer portal.

This is a string of the form “{client_id}:{client_secret}”.

>>> dd.plugins.ibanity.credentials
'a38f6dd1-7b48-4dbc-8493-2a142b6e134a:valid_client_secret'
>>> dd.plugins.ibanity.cert_file
PosixPath('.../secrets/certificate.pem')
>>> print(dd.plugins.ibanity.cert_file.read_text().strip())
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
>>> ses = dd.plugins.ibanity.get_ibanity_session()
>>> pprint(ses.get_access_token())
{'access_token': '...',
 'expires_in': 300,
 'not-before-policy': 0,
 'refresh_expires_in': 300,
 'refresh_token': '...',
 'scope': 'email profile',
 'session_state': '94bdf4ce-0dea-4ab7-abda-a53c7ba4bc87',
 'token_type': 'bearer'}

The above code snippet is a confirmation that your credentials are set up correctly. Congratulations! You won’t need this access token directly, but Lino will call it internally for every API call.

Suppliers

A supplier in Ibanity is the one sending and/or receiving an invoice via Peppol. Those are the customers of the hosting provider. Internally the Ibanity team uses the term “end user” instead of “supplier” and might change that at some point on the API too. An end user is the one using your software.

>>> list_suppliers = ses.list_suppliers()
>>> pprint(list_suppliers)
{'data': [{'attributes': {'city': 'Leuven',
                          'companyNumber': '1234567890',
                          'contactEmail': 'contact@example.be',
                          'country': 'Belgium',
                          'createdAt': '...',
                          'email': 'someone@example.be',
                          'enterpriseIdentification': {'enterpriseNumber': '1234567890',
                                                       'vatNumber': 'BE1234567890'},
                          'homepage': 'www.home.com',
                          'ibans': [{'id': 'bdfa52c6-2b50-4690-8b8d-24541a92c578',
                                     'value': 'BE68539007547034'},
                                    {'id': 'dcb9f7c2-be2c-4b52-8d77-3ed2bc05c5f8',
                                     'value': 'BE68539007547034'}],
                          'names': [{'id': '3dffba33-97af-4477-9fab-2d9d2dc31cee',
                                     'value': 'Company'},
                                    {'id': '99e410cc-d6f0-4f36-8096-949741ea8ec3',
                                     'value': 'Company S.A.'}],
                          'onboardingStatus': 'CREATED',
                          'peppolReceiver': False,
                          'phoneNumber': '+3254321121',
                          'street': 'Street',
                          'streetNumber': '2',
                          'supportEmail': 'support@example.be',
                          'supportPhone': '+3212345121',
                          'supportUrl': 'www.support.com',
                          'zip': '3000'},
           'id': '273c1bdf-6258-4484-b6fb-74363721d51f',
           'type': 'supplier'}],
 'meta': {'paging': {'number': 0, 'size': 2000, 'total': 1}}}
>>> for sup in list_suppliers['data']:
...    createdAt1 = sup['attributes'].pop('createdAt')
...    vat_id = sup['attributes']['enterpriseIdentification']['vatNumber']
...    sup_info = ses.get_supplier(sup['id'])
...    createdAt2 = sup_info['data']['attributes'].pop('createdAt')
...    assert sup_info['data']['attributes'] == sup['attributes']
...    print(f"Supplier {sup['id']} has VAT id {vat_id}")
Supplier 273c1bdf-6258-4484-b6fb-74363721d51f has VAT id BE1234567890
>>> from lino_xl.lib.ibanity.utils import DEMO_SUPPLIER_ID
>>> DEMO_SUPPLIER_ID
'273c1bdf-6258-4484-b6fb-74363721d51f'
>>> pprint(ses.get_supplier(DEMO_SUPPLIER_ID))
{'data': {'attributes': {'city': 'Leuven',
                         'companyNumber': '1234567890',
                         'contactEmail': 'contact@example.be',
                         'country': 'Belgium',
                         'createdAt': '...',
                         'email': 'someone@example.be',
                         'enterpriseIdentification': {'enterpriseNumber': '1234567890',
                                                      'vatNumber': 'BE1234567890'},
                         'homepage': 'www.home.com',
                         'ibans': [{'id': 'bdfa52c6-2b50-4690-8b8d-24541a92c578',
                                    'value': 'BE68539007547034'},
                                   {'id': 'dcb9f7c2-be2c-4b52-8d77-3ed2bc05c5f8',
                                    'value': 'BE68539007547034'}],
                         'names': [{'id': '3dffba33-97af-4477-9fab-2d9d2dc31cee',
                                    'value': 'Company'},
                                   {'id': '99e410cc-d6f0-4f36-8096-949741ea8ec3',
                                    'value': 'Company S.A.'}],
                         'onboardingStatus': 'CREATED',
                         'peppolReceiver': False,
                         'phoneNumber': '+3254321121',
                         'street': 'Street',
                         'streetNumber': '2',
                         'supportEmail': 'support@example.be',
                         'supportPhone': '+3212345121',
                         'supportUrl': 'www.support.com',
                         'zip': '3000'},
          'id': '273c1bdf-6258-4484-b6fb-74363721d51f',
          'type': 'supplier'}}

Let’s try to register a new supplier.

>>> rv = ses.create_supplier()
Traceback (most recent call last):
...
Exception: post https://api.ibanity.com/einvoicing/suppliers/ returned unexpected status code 500

That’s normal because we must supply the data.

>>> d = {}
>>> d["enterpriseIdentification"] = {
...     "enterpriseNumber": "1234567890",
...     "vatNumber": "BE1234567890"}
>>> d["contactEmail"] = "contact@example.be"
>>> d["ibans"] = [{"value": "BE68539007547034"}, {"value": "BE68539007547034"}]
>>> d["names"] = [{"value": "Company" }, {"value": "Company S.A."}]
>>> d["city"] = "Eupen"
>>> d["country"] = "Belgium"
>>> d["email"] = "someone@example.com"
>>> d["homepage"] = "www.home.com"
>>> d["phoneNumber"] = "+3287654312"
>>> d["street"] = "Neustraße"
>>> d["streetNumber"] = "123"
>>> d["supportEmail"] = "support@example.be"
>>> d["supportPhone"] = "+3212345121"
>>> d["supportUrl"] = "www.support.com"
>>> d["zip"] = "4700"
>>> d["peppolReceiver"] = True
>>> rv = ses.create_supplier(**d)
>>> pprint(rv)
{'data': {'attributes': {'city': 'Eupen',
                         'contactEmail': 'contact@example.be',
                         'country': 'Belgium',
                         'createdAt': '...',
                         'email': 'someone@example.com',
                         'enterpriseIdentification': {'enterpriseNumber': '1234567890',
                                                      'vatNumber': 'BE1234567890'},
                         'homepage': 'www.home.com',
                         'ibans': [{'id': '...', 'value': 'BE68539007547034'},
                                   {'id': '...', 'value': 'BE68539007547034'}],
                         'names': [{'id': '...', 'value': 'Company'},
                                   {'id': '...', 'value': 'Company S.A.'}],
                         'onboardingStatus': 'CREATED',
                         'peppolReceiver': True,
                         'phoneNumber': '+3287654312',
                         'street': 'Neustraße',
                         'streetNumber': '123',
                         'supportEmail': 'support@example.be',
                         'supportPhone': '+3212345121',
                         'supportUrl': 'www.support.com',
                         'zip': '4700'},
          'id': '...',
          'type': 'supplier'}}
>>> rv['data']['type']
'supplier'

The supplier_id of the newly created supplier is here:

>>> new_supplier_id = rv['data']['id']

We don’t test its value here because this value changes each time. But we might use it for retrieving the onboarding status of our new supplier.

>>> rv = ses.get_supplier(new_supplier_id)
>>> assert rv['data']['id'] == new_supplier_id

Unfortunately the Ibanity sandbox doesn’t actually store the new supplier; if we try to get it, we will still just get their plain old demo supplier:

>>> pprint(rv['data']['attributes']['city'])
'Leuven'
>>> pprint(rv['data']['attributes']['names'])
[{'id': '3dffba33-97af-4477-9fab-2d9d2dc31cee', 'value': 'Company'},
 {'id': '99e410cc-d6f0-4f36-8096-949741ea8ec3', 'value': 'Company S.A.'}]

Registrations

A registration is when an supplier has registered with an Access Point. The List Peppol Registrations returns a list of registrations for a given supplier.

>>> pprint(ses.list_registrations(DEMO_SUPPLIER_ID))
{'data': [{'attributes': {'accessPoints': ['Billit'],
                          'createdAt': '2023-08-16T12:38:16.662354Z',
                          'failedSince': '2023-08-16T12:38:16.662354Z',
                          'modifiedAt': '2023-08-16T12:38:22.575373Z',
                          'reason': 'already-registered',
                          'status': 'registration-failed',
                          'type': 'enterprise-number',
                          'value': '0143824670'},
           'id': '9d12d39d-2b03-4ea6-a770-f5d6b37edea7',
           'type': 'peppolRegistration'}]}

This API call requires a supplierId argument, but at least in the sandbox it then ignores this argument:

>>> reg1 = ses.list_registrations("123")
>>> reg2 = ses.list_registrations("456")
>>> reg1 == reg2
True

Documents

When talking to the Ibanity API about your invoices and other business documents, you first need to differentiate between “inbound” and “outbound” documents. Compare this to an email client where you have an “inbox” and an “outbox”.

Workflow for outbound documents:

  • Customer search 🡒 Customer reachability status (Document formats supported by this customer)

  • Send document 🡒 Receipt

  • Get feedback (ask status of an outbound document). The status is either successful, in which case we receive a transmissionID, or unsuccessful in which case we receive more details on the reason why it failed.

Workflow for inbound documents:

  • List suppliers 🡒 list of suppliers

  • List Peppol inbound documents (1 request per supplier) 🡒

  • Get Peppol inbound document (1 request per document) 🡒

Some properties are common to both inbound and outbound documents:

  • attributes.createdAt : when the document entered the Peppol network

  • relationships.supplier : the Peppol end point who posted this document into the Peppol network. This has nothing to do with the supplier on the invoice (who is called the seller)

  • id: unique identifier of this document.

  • attributes.transmissionId : an additional unique identifier within the Peppol network. In case of an issue this can be used in communication with the sending party.

  • the body of the document in UBL format

Outbound documents have three additional properties:

  • status: one of {created, sending, sent, invalid, send-error}

  • errors: one of {invalid-malicious, invalid-format, invalid-xsd, invalid-schematron, invalid-size, invalid-type, error-customer-not-registered, error-document-type-not-supported, error-customer-access-point-issue}

  • type: one of {“peppolInvoice”, “peppolOutboundDocument”, “peppolOutboundInvoice”, “peppolOutboundCreditNote”}

When asking for a list of documents, you specify a time range. For outbound documents this range means when their status changed (fromStatusChanged and toStatusChanged) while for inbound documents it means their creation time

You cannot create (post) an incoming document.

Outbound documents

>>> ar = rt.login("robin")
>>> qs = trading.VatProductInvoice.objects.filter(journal__ref="SLS")
>>> obj = qs.order_by("accounting_period__year", "number").last()
>>> obj
VatProductInvoice #106 ('SLS 10/2015')
>>> xmlfile, url = obj.make_xml_file(ar)
Make ...lino_book/projects/noi1e/settings/media/xml/2015/SLS-106.xml from SLS 10/2015 ...
Validate SLS-106.xml against .../lino_xl/lib/vat/XSD/PEPPOL-EN16931-UBL.sch ...
>>> res = ses.create_outbound_document(DEMO_SUPPLIER_ID, xmlfile)
>>> pprint(res)
{'data': {'attributes': {'createdAt': '2019-07-17T07:31:30.763402Z',
                         'status': 'created'},
          'id': '94884e80-cc4a-4583-bd4a-288095c7876f',
          'relationships': {'supplier': {'data': {'id': '273c1bdf-6258-4484-b6fb-74363721d51f',
                                                  'type': 'supplier'}}},
          'type': 'peppolInvoice'}}
>>> assert res['data']['attributes']['status'] == "created"
>>> assert res['data']['relationships']['supplier']['data']['id'] == DEMO_SUPPLIER_ID
>>> doc_id = res['data']['id']

The sandbox neither validates nor actually creates my document, it always returns a same document id. I can ask it to send another invoice and the sandbox will assign it the same doc_id as before.

>>> obj = trading.VatProductInvoice.objects.get(pk=105)
>>> xmlfile, url = obj.make_xml_file(ar)
Make ...lino_book/projects/noi1e/settings/media/xml/2015/SLS-105.xml from SLS 9/2015 ...
Validate SLS-105.xml against .../lino_xl/lib/vat/XSD/PEPPOL-EN16931-UBL.sch ...
>>> res = ses.create_outbound_document(DEMO_SUPPLIER_ID, xmlfile)
>>> assert res['data']['id'] == doc_id
>>> pprint(res)
{'data': {'attributes': {'createdAt': '2019-07-17T07:31:30.763402Z',
                         'status': 'created'},
          'id': '94884e80-cc4a-4583-bd4a-288095c7876f',
          'relationships': {'supplier': {'data': {'id': '273c1bdf-6258-4484-b6fb-74363721d51f',
                                                  'type': 'supplier'}}},
          'type': 'peppolInvoice'}}
>>> res = ses.get_outbound_document(DEMO_SUPPLIER_ID, doc_id)
>>> pprint(res)
{'data': {'attributes': {'createdAt': '2019-07-17T07:31:30.763402Z',
                         'errors': None,
                         'status': 'sent',
                         'transmissionId': 'ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5'},
          'id': '94884e80-cc4a-4583-bd4a-288095c7876f',
          'relationships': {'supplier': {'data': {'id': '273c1bdf-6258-4484-b6fb-74363721d51f',
                                                  'type': 'supplier'}}},
          'type': 'peppolInvoice'}}
>>> assert res['data']['id'] == doc_id

Also here, the sandbox “cheats”, it doesn’t check whether the requested document actually exists, it just returns a document descriptor for the requested id and type.

>>> res = ses.get_outbound_document(DEMO_SUPPLIER_ID, doc_id, credit_note=True)
>>> pprint(res)
{'data': {'attributes': {'createdAt': '2019-07-17T07:31:39.211221Z',
                         'errors': None,
                         'status': 'created',
                         'transmissionId': 'ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5'},
          'id': '94884e80-cc4a-4583-bd4a-288095c7876f',
          'relationships': {'supplier': {'data': {'id': '273c1bdf-6258-4484-b6fb-74363721d51f',
                                                  'type': 'supplier'}}},
          'type': 'peppolCreditNote'}}
>>> assert res['data']['id'] == doc_id
>>> doc_id = '12345678-1234-5678-bd4a-1234567890123'
>>> res = ses.get_outbound_document(DEMO_SUPPLIER_ID, doc_id, credit_note=True)
>>> res['data']['id']
'12345678-1234-5678-bd4a-1234567890123'
>>> start_time = datetime.datetime(2020,7,31,0,0,0)
>>> print(start_time.isoformat())
2020-07-31T00:00:00
>>> res = ses.list_outbound_documents(DEMO_SUPPLIER_ID, start_time)
>>> pprint(res)
{'data': [{'attributes': {'createdAt': '2020-07-31T00:00:00',
                          'status': 'created'},
           'id': '94884e80-cc4a-4583-bd4a-288095c7876f',
           'relationships': {'supplier': {'data': {'id': 'de142988-373c-4829-8181-92bdaf8ef26d',
                                                   'type': 'supplier'}}},
           'type': 'peppolInvoice'}],
 'meta': {'paging': {'number': 0, 'size': 2000, 'total': 1}}}

Inbound documents

>>> res = ses.list_inbound_documents(DEMO_SUPPLIER_ID)
>>> sorted(res.keys())
['data', 'meta']
>>> pprint(res['data'])
[{'attributes': {'createdAt': '...',
                 'transmissionId': 'c038dbdc1-26ed-41bf-9ebf-37g3c4ceaa58'},
  'id': '431cb851-5bb2-4526-8149-5655d648292f',
  'relationships': {'supplier': {'data': {'id': 'de142988-373c-4829-8181-92bdaf8ef26d',
                                          'type': 'supplier'}}},
  'type': 'peppolInboundDocument'}]
>>> doc_info = res['data'][0]
>>> doc_id = doc_info['id']
>>> res = ses.get_inbound_document_xml(doc_id)
>>> pprint(res)
('<?xml version="1.0" encoding="UTF-8"?>\n'
 '     <Invoice '
 'xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"\n'
 '       '
 'xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"\n'
 '       xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">\n'
 '      </Invoice>')

Class reference

The ibanity plugin injects a checkbox field “is_outbound” into contacts.Journal and into contacts.Partner. In the demo data this field is checked (1) for journal SLS and (2) for some partners (a subset of those with a vat_id).

class lino_xl.lib.contacts.Partner
Noindex:

is_outbound

Whether sales invoices and creditnoutes to this partner should be sent via the Peppol network.

class lino_xl.lib.ibanity.OnboardingStates

A choicelist with values for the Supplier.onboarding_state.

class lino_xl.lib.ibanity.Supplier

Django model used to represent an Ibanity supplier

class lino_xl.lib.ibanity.CustomerStates

A choicelist with three values:

  • active: The customer is already actively using Peppol and wants to receive all his invoices via Peppol, including your documents.

  • potential: The customer can be reached on Peppol, but did not yet confirm to receive your documents via Peppol.

  • unreachable: The customer is not on Peppol, for example because his bank application is not connected to Peppol yet.

class lino_xl.lib.ibanity.SuppliersListChecker

Checks whether all suppliers that were registered by this Ibanity client have a Supplier row on this Lino site.

class lino_xl.lib.ibanity.SupplierChecker

Synchronizes this Supplier row with the data registered in the Ibanity API.

Note that SuppliersListChecker and SupplierChecker are manual checkers. We do not want these checkers to run automatically during checkdata, only when called manually, because it requires Ibanity credentials, which are not available e.g. on GitLab.

Plugin settings

lino_xl.lib.ibanity.supplier_id

The identification code of this Lino site as an Ibanity supplier.

lino_xl.lib.ibanity.with_suppliers

Whether this site can register other organizations as Ibanity suppliers.

The following three settings are filled automatically by the plugin at startup:

lino_xl.lib.ibanity.credentials

The Ibanity credentials to use on this Lino site for accessing the Ibanity API.

Must be specified in the form "{client_id}:{client_secret}".

lino_xl.lib.ibanity.cert_file

Certification file to use for connecting to Ibanity.

lino_xl.lib.ibanity.key_file

Private key file to use for connecting to Ibanity.