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