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

Send and receive Peppol documents

With this usage scenario of the lino_xl.lib.peppol plugin you can send and receive your invoices and credit notes via the Peppol network.

To activate this scenario, set the supplier_id plugin setting to the supplier id you received from your Ibanity hosting provider who registered you as an Ibanity supplier.

All code snippets on this page (lines starting with >>>) are being tested as part of our development workflow. The following snippet initializes a demo project to use throughout this page.

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

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

>>> if dd.plugins.peppol.credentials is None:
...     pytest.skip('this doctest requires Ibanity credentials')
>>> translation.activate('en')
>>> outbound_model = dd.plugins.peppol.outbound_model

The test snippets in is document write to the database, that’s why we tidy up after previous test runs:

>>> import shutil
>>> from lino.core.gfks import gfk2lookup
>>> def tidy_up():
...     for obj in peppol.InboundDocument.objects.filter(voucher_id__isnull=False):
...         flt = gfk2lookup(uploads.Upload.owner, obj.voucher)
...         uploads.Upload.objects.filter(**flt).delete()
...         obj.voucher.delete()
...     peppol.InboundDocument.objects.all().delete()
...     shutil.rmtree(dd.plugins.peppol.inbox_dir, ignore_errors=True)
...     peppol.OutboundDocument.objects.all().delete()
...     qs = outbound_model.objects.filter(state=accounting.VoucherStates.sent)
...     qs.update(state=accounting.VoucherStates.registered)
...     excerpts.Excerpt.objects.filter(id__gt=6).delete()
...     contacts.Partner.objects.exclude(peppol_id='').update(peppol_id='')

We need to write our own tidy_up() function because lino.utils.dbhash.check_virgin() doesn’t tidy up because rows that have been updated.

>>> tidy_up()

Database models

The peppol plugin injects a checkbox field “is_outbound” into accounting.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.peppol.PeppolPartner
is_outbound

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

peppol_id

How this partner identifies themselves in the Peppol network. This is a string of style schemaID:value, where schemaID refers to a EAS.

class lino_xl.lib.peppol.PeppolJournal
is_outbound

Whether vouchers of this journal should be sent via the Peppol network.

Suppliers management

The cosi1 demo sites has no suppliers management, this fictive company has just received a supplier_id from their Lino provider.

>>> ar = rt.login("robin")
>>> dd.plugins.peppol.with_suppliers
False
>>> ar.show(peppol.OnboardingStates)
Traceback (most recent call last):
...
AttributeError: module 'lino_xl.lib.peppol.models' has no attribute 'OnboardingStates'...
>>> ar.show(peppol.Suppliers)
Traceback (most recent call last):
...
AttributeError: module 'lino_xl.lib.peppol.models' has no attribute 'Suppliers'
>>> dd.plugins.peppol.supplier_id
'273c1bdf-6258-4484-b6fb-74363721d51f'

Outbound documents

In the beginning our Outbox is empty:

>>> rt.show(peppol.Outbox)
No data to display

Fill the outbox with invoices to send:

>>> with ar.print_logger("DEBUG"):
...     rt.models.peppol.collect_outbound(ar)
Collect outbound invoices into outbox
Scan 1 outbound journal(s): ['SLS']
Collect 5 new invoices into outbox
...
>>> rt.show(peppol.Outbox)
==================== ===================== ================ ============ ================= ==============
 Sales invoice        Partner               VAT regime       Entry date   Total excl. VAT   VAT
-------------------- --------------------- ---------------- ------------ ----------------- --------------
 SLS 1/2014           Bestbank              Subject to VAT   07/01/2014   2 999,85          629,97
 SLS 3/2014           Bäckerei Ausdemwald   Subject to VAT   09/01/2014   679,81            142,76
 SLS 4/2014           Bäckerei Mießen       Subject to VAT   10/01/2014   280,00            58,80
 SLS 5/2014           Bäckerei Schmitz      Subject to VAT   11/01/2014   535,00            112,35
 SLS 6/2014           Garage Mergelsberg    Subject to VAT   07/02/2014   1 110,16          203,87
 **Total (5 rows)**                                                       **5 604,82**      **1 147,75**
==================== ===================== ================ ============ ================= ==============
>>> ses = dd.plugins.peppol.get_ibanity_session()

Send outbound documents:

>>> with ar.print_logger("DEBUG"):
...     peppol.send_outbound(ses, ar)
...
Send outbound documents
Gonna send SLS 1/2014
Make .../media/xml/2014/SLS-2014-1.xml from SLS 1/2014 ...
Made .../media/xml/2014/SLS-2014-1.xml
Ibanity response {'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'}
Gonna send SLS 3/2014
Make .../media/xml/2014/SLS-2014-3.xml from SLS 3/2014 ...
Made .../media/xml/2014/SLS-2014-3.xml
Ibanity response {'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'}
Gonna send SLS 4/2014
Make .../media/xml/2014/SLS-2014-4.xml from SLS 4/2014 ...
Made .../media/xml/2014/SLS-2014-4.xml
Ibanity response {'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'}
Gonna send SLS 5/2014
Make .../media/xml/2014/SLS-2014-5.xml from SLS 5/2014 ...
Made .../media/xml/2014/SLS-2014-5.xml
Ibanity response {'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'}
Gonna send SLS 6/2014
Make .../media/xml/2014/SLS-2014-6.xml from SLS 6/2014 ...
Made .../media/xml/2014/SLS-2014-6.xml
Ibanity response {'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'}
>>> rt.show(excerpts.Excerpts, column_names="excerpt_type owner")
========================= ====================
 Excerpt Type              Controlled by
------------------------- --------------------
 Sales invoice             `SLS 6/2014 <…>`__
 Sales invoice             `SLS 5/2014 <…>`__
 Sales invoice             `SLS 4/2014 <…>`__
 Sales invoice             `SLS 3/2014 <…>`__
 Payment reminder          `Bestbank <…>`__
 Sales invoice             `SLS 1/2014 <…>`__
 Payment Order             `PMO 1/2014 <…>`__
 Journal Entry             `PRE 1/2014 <…>`__
 Bank Statement            `BNK 1/2014 <…>`__
 Belgian VAT declaration   `VAT 1/2014 <…>`__
========================= ====================

The Outbox table is now empty, and the invoices have moved to the Sent table.

>>> rt.show(peppol.Outbox)
No data to display
>>> rt.show(peppol.Sent)
=============== ===================== ============================ ========= =================
 Sales invoice   Partner               Created at                   State     Transmission ID
--------------- --------------------- ---------------------------- --------- -----------------
 SLS 1/2014      Bestbank              2019-07-17 07:31:30.763402   Created
 SLS 3/2014      Bäckerei Ausdemwald   2019-07-17 07:31:30.763402   Created
 SLS 4/2014      Bäckerei Mießen       2019-07-17 07:31:30.763402   Created
 SLS 5/2014      Bäckerei Schmitz      2019-07-17 07:31:30.763402   Created
 SLS 6/2014      Garage Mergelsberg    2019-07-17 07:31:30.763402   Created
=============== ===================== ============================ ========= =================

The status is “created” and they do not yet have any transmission ID. This will change with the next synchronization step:

>>> with ar.print_logger("DEBUG"):
...     peppol.followup_outbound(ses, ar)
...
Check status of sent documents
SLS 1/2014 (ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5) state created becomes sent
SLS 3/2014 (ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5) state created becomes sent
SLS 4/2014 (ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5) state created becomes sent
SLS 5/2014 (ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5) state created becomes sent
SLS 6/2014 (ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5) state created becomes sent
>>> rt.show(peppol.Sent)
=============== ===================== ============================ ======= =======================================
 Sales invoice   Partner               Created at                   State   Transmission ID
--------------- --------------------- ---------------------------- ------- ---------------------------------------
 SLS 1/2014      Bestbank              2019-07-17 07:31:30.763402   Sent    ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5
 SLS 3/2014      Bäckerei Ausdemwald   2019-07-17 07:31:30.763402   Sent    ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5
 SLS 4/2014      Bäckerei Mießen       2019-07-17 07:31:30.763402   Sent    ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5
 SLS 5/2014      Bäckerei Schmitz      2019-07-17 07:31:30.763402   Sent    ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5
 SLS 6/2014      Garage Mergelsberg    2019-07-17 07:31:30.763402   Sent    ba6c26fa-f47c-4ef1-866b-71e4ef02f15a5
=============== ===================== ============================ ======= =======================================

Inbound documents

In the beginning our Inbox is empty:

>>> rt.show(peppol.Inbox)
No data to display

Checking our inbox means that we ask whether we have received any new inbound documents.

>>> with ar.print_logger("DEBUG"):
...     peppol.check_inbox(ses, ar)
...
Check for new inbound documents
We got a new document 431cb851-5bb2-4526-8149-5655d648292f

New documents are now visible in our database:

>>> rt.show(peppol.Inbox)
============================ ======================================= ========= ========= ========
 Created at                   Transmission ID                         Invoice   Partner   Amount
---------------------------- --------------------------------------- --------- --------- --------
 ...                          c038dbdc1-26ed-41bf-9ebf-37g3c4ceaa58
============================ ======================================= ========= ========= ========

Now Lino can download the detail of every single document.

>>> with ar.print_logger("DEBUG"):
...     peppol.download_inbound(ses, ar)
...
Download inbound documents
Found 1 inbound documents to download
Download 431cb851-5bb2-4526-8149-5655d648292f
It's an invoice
Unknown Peppol ID 9931:100588749
Assign 9931:100588749 to partner Rumma & Ko OÜ because name matches
Supplier 9931:100588749 is Rumma & Ko OÜ
Lino ignores information in Metal chair 79.99
Lino ignores information in Website hosting 1MB/month 3.99
Lino ignores information in IT consultation & maintenance 30.00
Lino ignores information in <cbc:ID>SLS 3/2014</cbc:ID>
Store embedded file (PDF version) SLS-2014-3.pdf
Created INB 1/2014 from 431cb851-5bb2-4526-8149-5655d648292f

Now the Lino invoice has been created but is not yet registered.

>>> rt.show(peppol.Inbox)
============================ ======================================= ============ =============== ============
 Created at                   Transmission ID                         Invoice      Partner         Amount
---------------------------- --------------------------------------- ------------ --------------- ------------
 ...                          c038dbdc1-26ed-41bf-9ebf-37g3c4ceaa58   INB 1/2014   Rumma & Ko OÜ   822,57
 **Total (1 rows)**                                                                                **822,57**
============================ ======================================= ============ =============== ============
>>> print(dd.plugins.peppol.inbox_dir)
/.../projects/cosi1/media/ibanity_inbox
>>> for fn in dd.plugins.peppol.inbox_dir.iterdir():
...     print(fn)
/.../projects/cosi1/media/ibanity_inbox/431cb851-5bb2-4526-8149-5655d648292f.xml

Choicelists

class lino_xl.lib.peppol.OutboundStates
>>> rt.show(peppol.OutboundStates)
======= ============ ============
 value   name         text
------- ------------ ------------
 10      created      Created
 20      sending      Sending
 30      sent         Sent
 40      invalid      Invalid
 50      send_error   Send-Error
======= ============ ============
>>> rt.show(peppol.OutboundErrors)
======= ========================= =========================
 value   name                      text
------- ------------------------- -------------------------
 010     malicious                 Malicious
 020     format                    Invalid format
 030     xsd                       Invalid XML
 040     schematron                Invalid Schematron
 050     identifiers               Invalid identifiers
 060     size                      Invalid size
 070     invalid_type              Invalid type
 080     customer_not_registered   Customer not registered
 090     unsupported               Type not supported
 100     access_point              Access Point issue
 110     unspecified               Unspecified error
======= ========================= =========================

At the end of this page we tidy up the database to avoid side effects in following tests:

>>> tidy_up()
>>> dbhash.check_virgin()