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.

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.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()
...     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 defines two model mixins that add a fields to lino_xl.lib.accounting.Journal and lino_xl.lib.contacts.Partner.

class lino_xl.lib.peppol.PeppolPartner
send_peppol

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

In the demo data this field is checked for some partners (a subset of those with a vat_id).

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.

In the demo data this field is checked for journal SLS.

class lino_xl.lib.peppol.PeppolJournal
is_outbound

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

Data tables

class lino_xl.lib.peppol.Inbox

Shows the Peppol documents that were received and have not yet been processed.

class lino_xl.lib.peppol.Archive

Shows the Peppol documents that were received and have been processed.

class lino_xl.lib.peppol.Outbox

Shows the documents to be sent to the Peppol network.

class lino_xl.lib.peppol.Sent

Shows the documents that have been sent to 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
>>> ar.show(peppol.Outbox)
==================== ===================== ================ ============ ================= ==============
 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(ar)

Send outbound documents:

>>> with ar.print_logger("INFO"):
...     peppol.send_outbound(ses)
...
Send outbound documents
weasy2pdf render .../media/cache/weasy2pdf/SLS-2014-3.pdf ('de', {})
weasy2pdf render .../media/cache/weasy2pdf/SLS-2014-4.pdf ('de', {})
weasy2pdf render .../media/cache/weasy2pdf/SLS-2014-5.pdf ('de', {})
weasy2pdf render .../media/cache/weasy2pdf/SLS-2014-6.pdf ('de', {})
>>> rt.show(excerpts.Excerpts, column_names="excerpt_type owner")
========================= ====================
 Excerpt Type              Controlled by
------------------------- --------------------
 Trading invoice           `SLS 6/2014 <…>`__
 Trading invoice           `SLS 5/2014 <…>`__
 Trading invoice           `SLS 4/2014 <…>`__
 Trading invoice           `SLS 3/2014 <…>`__
 Payment reminder          `Bestbank <…>`__
 Trading 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)
============ ===================== ============================ ========= =================
 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("INFO"):
...     peppol.followup_outbound(ses)
...
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)
============ ===================== ============================ ======= =======================================
 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("INFO"):
...     peppol.check_inbox(ses)
...
Check for new inbound documents
Receive 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("INFO"):
...     peppol.download_inbound(ses)
...
Download inbound documents
Found 1 inbound documents to download
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
======= ============ ============

The following choicelist is not needed because we just store the text version in OutboundDocument.error_message.

>>> 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()