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

Ibanity API (part 1) : suppliers

This section explores the supplier management subset of the Ibanity API, which is used internally by the lino_xl.lib.peppol plugin. As a Lino application developer you won’t need to know these details if you just use the plugin.

About this document

This page contains code snippets (lines starting with >>>), which are being tested during our development workflow. The following snippet initializes the demo project used throughout this page.

>>> from lino_book.projects.noi1r.startup import *
>>> pytest.skip("Code snippets in this document currently aren't tested")

The code snippets in this document are tested only if you have Ibanity credentials installed:

>>> if not dd.plugins.peppol.credentials:
...     pytest.skip('this doctest requires Ibanity credentials')
>>> from lino_xl.lib.peppol.utils import supplier_attrs, res2str, EndpointID

Tidying up from previous runs is more complicated for this doctest because it communicates with the Ibanity environment.

>>> ar = rt.login("robin")
>>> ses = dd.plugins.peppol.get_ibanity_session(ar)
>>> def tidy_up_ibanity():
...     eid = EndpointID("BE0404484654")
...     if (data := ses.find_supplier_by_eid(eid)) is not None:
...         ses.delete_supplier(data['id'])
>>> tidy_up_ibanity()

Access token

The peppol.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.peppol.credentials
'6b6720e4-bed2-4272-ab77-f534bab6dcc7:AJib13J5MiVBHjGLSImHd1dDlyJvtGPE'
>>> dd.plugins.peppol.cert_file
PosixPath('.../secrets/certificate.pem')
>>> print(dd.plugins.peppol.cert_file.read_text().strip())
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
>>> pprint(ses.get_access_token())
{'access_token': '...',
 'expires_in': 300,
 'not-before-policy': 0,
 'refresh_expires_in': 0,
 'scope': 'email profile',
 '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

Lino’s Supplier model matches what Ibanity calls a Supplier resource

The lino_book.projects.noi1e demo site contains three fictive Ibanity suppliers, which have been created in the Ibanity environment when we run pm prep.

List suppliers

>>> lst = list(ses.list_suppliers())
>>> len(lst) == 3
True
>>> pprint(lst[0])
{'attributes': {'city': 'Eupen',
                'companyNumber': '0345678997',
                'contactEmail': 'info@example.com',
                'country': 'Belgium',
                'createdAt': '...',
                'email': 'info@example.com',
                'enterpriseIdentification': {'enterpriseNumber': '0345678997',
                                             'vatNumber': 'BE0345678997'},
                'homepage': 'https://',
                'ibans': [{'id': '...',
                           'value': 'BE86973367680150'}],
                'names': [{'id': '...',
                           'value': 'Number Three'}],
                'onboardingStatus': 'ONBOARDED',
                'peppolReceiver': False,
                'phoneNumber': '+3223344556',
                'street': 'Peppolstraße',
                'streetNumber': '34',
                'supportEmail': '',
                'supportPhone': '',
                'supportUrl': 'https://www.saffre-rumma.net/',
                'zip': '4700'},
  'id': '...',
  'type': 'supplier'}

Look up a supplier

>>> DEMO_SUPPLIER_ID = lst[0]['id']
>>> data, errmsg = ses.get_supplier(DEMO_SUPPLIER_ID)
>>> assert errmsg is None
>>> pprint(data)
...
{'attributes': {'city': 'Eupen',
                'companyNumber': '0345678997',
                'contactEmail': 'info@example.com',
                'country': 'Belgium',
                'createdAt': '...',
                'email': 'info@example.com',
                'enterpriseIdentification': {'enterpriseNumber': '0345678997',
                                             'vatNumber': 'BE0345678997'},
                'homepage': 'https://',
                'ibans': [{'id': '...',
                           'value': 'BE86973367680150'}],
                'names': [{'id': '...',
                           'value': 'Number Three'}],
                'onboardingStatus': 'ONBOARDED',
                'peppolReceiver': False,
                'phoneNumber': '+3223344556',
                'street': 'Peppolstraße',
                'streetNumber': '34',
                'supportEmail': '',
                'supportPhone': '',
                'supportUrl': 'https://www.saffre-rumma.net/',
                'zip': '4700'},
 'id': '...',
 'type': 'supplier'}
>>> data, errmsg = ses.get_supplier('123456-789-abc')
>>> assert data is None
>>> print(errmsg)
{'code': 'invalidParameter', 'detail': "The parameter 'supplierId' expected type is: 'uuid'"}

Create a supplier

Let’s try to register a new supplier.

>>> d = supplier_attrs("BE1234567890")

Lino currently supports only countries that identify using VAT id. Enterprise number is not used. In the future we might want to have both:

>>> pprint(d)
{'enterpriseIdentification': {'enterpriseNumber': '1234567890',
                              'vatNumber': 'BE1234567890'}}

The enterpriseIdentification is not enough:

>>> data, errmsg = ses.create_supplier(**d)
>>> print(errmsg)
city: must not be blank, contactEmail: must not be blank, country: must not be
blank, email: must not be blank, homepage: must not be null, ibans: must not be
empty, names: must not be empty, phoneNumber: must not be blank, street: must
not be blank, streetNumber: must not be blank, zip: must not be blank
>>> d["contactEmail"] = "contact@example.be"
>>> d["names"] = [{"value": "Company" }, {"value": "Company S.A."}]
>>> d["ibans"] = [{"value": "BE68539007547034"}, {"value": "BE38248017357572"}]
>>> d["city"] = "Eupen"
>>> d["country"] = "Belgium"
>>> d["email"] = "someone@example.com"
>>> d["homepage"] = "https://www.example.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

Even now that we specify all required data, it fails because our vatNumber is invalid:

>>> data, errmsg = ses.create_supplier(**d)
>>> assert data is None
>>> print(errmsg)
enterpriseIdentification/enterpriseNumber: must be a valid Belgian enterprise number, enterpriseIdentification/vatNumber: must be a valid Belgian VAT number

It’s not allowed to create a supplier for a company who is already registered at another Peppol Access Point:

>>> d = supplier_attrs("BE0650238114", **d)
>>> data, errmsg = ses.create_supplier(**d)
...
Traceback (most recent call last):
...
lino_xl.lib.peppol.utils.PeppolFailure: POST https://api.ibanity.com/einvoicing/suppliers/ () returned 409
{"errors":[{"code":"alreadyRegistered","detail":"A supplier already exists with this enterprise identification"}]} (options were {'json': {'data': {'type': 'supplier', 'attributes': {'enterpriseIdentification': {'enterpriseNumber': '0650238114', 'vatNumber': 'BE0650238114'}, 'contactEmail': 'contact@example.be', 'names': [{'value': 'Company'}, {'value': 'Company S.A.'}], 'ibans': [{'value': 'BE68539007547034'}, {'value': 'BE38248017357572'}], 'city': 'Eupen', 'country': 'Belgium', 'email': 'someone@example.com', 'homepage': 'https://www.example.com', 'phoneNumber': '+3287654312', 'street': 'Neustraße', 'streetNumber': '123', 'supportEmail': 'support@example.be', 'supportPhone': '+3212345121', 'supportUrl': 'www.support.com', 'zip': '4700', 'peppolReceiver': True}}},
...)

Here is a valid VAT number of a real company (called SA ETHIAS, found on Internet) and we try to create a supplier from it with our fictive data:

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

Above code snippet is skipped because the Ibanity API doesn’t provide a way to remove a supplier. If we really ran that request on each test run, the Ibanity environment would grow in an uncontrolled way.

List Peppol Registrations

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

>>> lst = list(ses.list_registrations(DEMO_SUPPLIER_ID))

The result looks as follows, but we cannot test this here because it depends on previous test runs.

>>> pprint(lst)
{'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'}]}

The ibans and names fields

The Ibanity docs describe how Lino must submit changes in the the ibans and names fields. The following snippets verify the rules.

>>> from lino_xl.lib.peppol.suppliers import update_id_list
>>> oldlist = [{'id': '123', 'value':'ABC'}]
>>> update_id_list(oldlist, "ABC")
[{'id': '123', 'value': 'ABC'}]
>>> update_id_list(oldlist, "ABC;DEF")
[{'id': '123', 'value': 'ABC'}, {'value': 'DEF'}]
>>> update_id_list(oldlist, "")
[]
>>> oldlist = []
>>> update_id_list(oldlist, "ABC")
[{'value': 'ABC'}]
>>> update_id_list(oldlist, "ABC;DEF")
[{'value': 'ABC'}, {'value': 'DEF'}]
>>> update_id_list(oldlist, "")
[]

The list_suppliers admin command

The list_suppliers admin command lists the Ibanity suppliers defined in the API environment connected to this Lino site.

list_suppliers

Example run:

>>> from atelier.sheller import Sheller
>>> shell = Sheller(settings.SITE.project_dir)
>>> shell('python manage.py list_suppliers')
...
1) ... BE0345678997 ONBOARDED (Number Three)
2) ... BE0234567873 ONBOARDED (Number Two)
3) ... BE0123456749 ONBOARDED (Number One)