Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature : zugferd 2.0 #21

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Factur-X Python library
Factur-X is the e-invoicing standard for France and Germany. The Factur-X specifications are available on the `FNFE-MPE website <http://fnfe-mpe.org/factur-x/>`_ in English and French. The Factur-X standard is also called `ZUGFeRD 2.1 in Germany <https://www.ferd-net.de/standards/zugferd-2.1.1/index.html>`_.

The main feature of this Python library is to generate Factur-X invoices from a regular PDF invoice and a Factur-X compliant XML file.
Also it's possible to choose to generate Factur-x invoices matching the ZUGFeRD 2.0 standard (instead of ZUGFeRD 2.1), even though it's not recommended.

This lib provides additionnal features such as:

Expand Down Expand Up @@ -55,14 +56,16 @@ All these commande line tools have a **-h** option that explains how to use them
Webservice
==========

This project also provides a webservice to generate a Factur-X invoice from a regular PDF invoice, the *factur-x.xml* file and additional attachments (if any). This webservice runs on Python3 and uses `Flask <https://www.palletsprojects.com/p/flask/>`_. To run the webservice, run **facturx-webservice** available in the *bin* subdirectory of the project. To query the webservice, you must send an **HTTP POST** request in **multipart/form-data** using the following keys:
This project also provides a webservice to generate a Factur-X invoice from a regular PDF invoice, the *factur-x.xml* file and additional attachments (if any). This webservice runs on Python3 and uses `Flask <https://www.palletsprojects.com/p/flask/>`_. To run the webservice, run **facturx-webservice** available in the *bin* subdirectory of the project. To query the webservice, you must send an **HTTP POST** request at **/generate_facturx** in **multipart/form-data** using the following keys:

* **pdf** -> PDF invoice (required)
* **xml** -> factur-x.xml file (any profile, required)
* **attachment1** -> First attachment (optional)
* **attachment2** -> Second attachment (optional)
* ...

It's also possible to give the parameter **zugferd_version** in the url (**/generate_facturx?zugferd_version=2.0**) to choose between ZUGFeRD 2.0 and 2.1 (this last one beeing the default value).

It is recommended to run the webservice behind an HTTPS/HTTP proxy such as `Nginx <https://www.nginx.com/>`_ or `Apache <https://httpd.apache.org/>`_. You will certainly have to increase the default maximum upload size (default value is only 1MB under Nginx!): use the parameter **client_max_body_size** for Nginx and **LimitRequestBody** for Apache.

You can use `curl <https://curl.haxx.se/>`_, a command line tool to send HTTP requests (on Linux Ubuntu/Debian, just install the **curl** package) to generate the request:
Expand All @@ -86,6 +89,10 @@ Contributors
Changelog
=========

* Version 1.13 dated 2020-12-16

* Allow generating factur-x invoices matching the ZUGFeRD 2.0 standard

* Version 1.12 dated 2020-07-16

* Compress attachments and XMP metadata using Flate compression
Expand Down
10 changes: 9 additions & 1 deletion bin/facturx-pdfgen
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ from optparse import OptionParser
import sys
from facturx import generate_facturx_from_file
from facturx.facturx import logger
from facturx.facturx import ZUGFERD_VERSIONS
import logging
from os.path import isfile, isdir, basename

Expand All @@ -18,6 +19,10 @@ options = [
'action': 'store', 'default': 'info',
'help': "Set log level. Possible values: debug, info, warn, error. "
"Default value: info."},
{'names': ('-z', '--zugferd-version'), 'dest': 'zugferd_version',
'action': 'store', 'default': '2.1',
'help': "Choose to match either ZUGFeRD 2.1 or 2.0. "
"Default: 2.1"},
{'names': ('-d', '--disable-xsd-check'), 'dest': 'disable_xsd_check',
'action': 'store_true', 'default': False,
'help': "De-activate XML Schema Definition check on Factur-X XML file "
Expand Down Expand Up @@ -94,6 +99,9 @@ def main(options, arguments):
if options.disable_xsd_check:
check_xsd = False
pdf_metadata = None
if options.zugferd_version not in ZUGFERD_VERSIONS:
logger.error('The version %s is not allowed. Exit.', options.zugferd_version)
sys.exit(1)
if (
options.meta_author or
options.meta_keywords or
Expand Down Expand Up @@ -123,7 +131,7 @@ def main(options, arguments):
generate_facturx_from_file(
pdf_filename, xml_file, check_xsd=check_xsd,
facturx_level=options.facturx_level, pdf_metadata=pdf_metadata,
output_pdf_file=facturx_pdf_filename, attachments=attachments)
output_pdf_file=facturx_pdf_filename, attachments=attachments, zugferd_version=options.zugferd_version)
except Exception as e:
logger.error('Factur-x lib call failed. Error: %s', e)
sys.exit(1)
Expand Down
10 changes: 9 additions & 1 deletion bin/facturx-webservice
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ from flask import Flask, request, send_file
from tempfile import NamedTemporaryFile
from facturx import generate_facturx_from_file
from facturx.facturx import logger as fxlogger
from facturx.facturx import ZUGFERD_VERSIONS
from optparse import OptionParser
import logging
from logging.handlers import RotatingFileHandler
Expand All @@ -21,6 +22,13 @@ app = Flask(__name__)

@app.route('/generate_facturx', methods=['POST'])
def generate_facturx():
zugferd_version = request.args.get('zugferd_version', default=ZUGFERD_VERSIONS[0], type=str)
if zugferd_version not in ZUGFERD_VERSIONS:
app.logger.warning('The version %s is not allowed. Using %s instead.',
zugferd_version, ZUGFERD_VERSIONS[0])
zugferd_version = ZUGFERD_VERSIONS[0]
app.logger.debug('zugferd_version=%s', zugferd_version)

app.logger.debug('request.files=%s', request.files)
attachments = {}
for i in range(MAX_ATTACHMENTS):
Expand Down Expand Up @@ -50,7 +58,7 @@ def generate_facturx():
app.logger.debug('attachments keys=%s', attachments.keys())
generate_facturx_from_file(
pdf_file, xml_byte, output_pdf_file=output_pdf_file.name,
attachments=attachments)
attachments=attachments, zugferd_version=zugferd_version)
output_pdf_file.seek(0)
res = send_file(output_pdf_file.name, as_attachment=True)
app.logger.info(
Expand Down
32 changes: 25 additions & 7 deletions facturx/facturx.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
logger = logging.getLogger('factur-x')
logger.setLevel(logging.INFO)

ZUGFERD_VERSIONS = ['2.1', '2.0']
FACTURX_FILENAME = 'factur-x.xml'
ZUGFERD_FILENAMES = ['zugferd-invoice.xml', 'ZUGFeRD-invoice.xml']
FACTURX_LEVEL2xsd = {
Expand Down Expand Up @@ -329,7 +330,7 @@ def _prepare_pdf_metadata_txt(pdf_metadata):
return info_dict


def _prepare_pdf_metadata_xml(facturx_level, pdf_metadata):
def _prepare_pdf_metadata_xml(facturx_level, pdf_metadata, zugferd_version=ZUGFERD_VERSIONS[0]):
xml_str = u"""
<?xpacket begin="\ufeff" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
Expand Down Expand Up @@ -405,13 +406,20 @@ def _prepare_pdf_metadata_xml(facturx_level, pdf_metadata):
<rdf:Description xmlns:fx="urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#" rdf:about="">
<fx:DocumentType>{facturx_documenttype}</fx:DocumentType>
<fx:DocumentFileName>{facturx_filename}</fx:DocumentFileName>
<fx:Version>{facturx_version}</fx:Version>
<fx:Version>1.0</fx:Version>
<fx:ConformanceLevel>{facturx_level}</fx:ConformanceLevel>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
"""
if zugferd_version == '2.0':
facturx_filename = ZUGFERD_FILENAMES[0]
elif zugferd_version == '2.1':
facturx_filename = FACTURX_FILENAME
else:
facturx_filename = FACTURX_FILENAME

xml_str = xml_str.format(
title=pdf_metadata.get('title', ''),
author=pdf_metadata.get('author', ''),
Expand All @@ -420,7 +428,7 @@ def _prepare_pdf_metadata_xml(facturx_level, pdf_metadata):
creator_tool='factur-x python lib v%s by Alexis de Lattre' % __version__,
timestamp=_get_metadata_timestamp(),
facturx_documenttype='INVOICE',
facturx_filename=FACTURX_FILENAME,
facturx_filename=facturx_filename,
facturx_version='1.0',
facturx_level=FACTURX_LEVEL2xmp[facturx_level])
xml_byte = xml_str.encode('utf-8')
Expand Down Expand Up @@ -482,7 +490,8 @@ def _filespec_additional_attachments(

def _facturx_update_metadata_add_attachment(
pdf_filestream, facturx_xml_str, pdf_metadata, facturx_level,
output_intents=[], additional_attachments={}):
output_intents=[], additional_attachments={},
zugferd_version=ZUGFERD_VERSIONS[0]):
'''This method is inspired from the code of the addAttachment()
method of the PyPDF2 lib'''
# The entry for the file
Expand Down Expand Up @@ -554,7 +563,8 @@ def _facturx_update_metadata_add_attachment(
output_intent_obj = pdf_filestream._addObject(output_intent_dict)
res_output_intents.append(output_intent_obj)
# Update the root
metadata_xml_str = _prepare_pdf_metadata_xml(facturx_level, pdf_metadata)
metadata_xml_str = _prepare_pdf_metadata_xml(
facturx_level, pdf_metadata, zugferd_version=zugferd_version)
metadata_file_entry = DecodedStreamObject()
metadata_file_entry.setData(metadata_xml_str)
metadata_file_entry = metadata_file_entry.flateEncode()
Expand Down Expand Up @@ -741,7 +751,8 @@ def generate_facturx_from_binary(
def generate_facturx_from_file(
pdf_invoice, facturx_xml, facturx_level='autodetect',
check_xsd=True, pdf_metadata=None, output_pdf_file=None,
additional_attachments=None, attachments=None):
additional_attachments=None, attachments=None,
zugferd_version=ZUGFERD_VERSIONS[0]):
"""
Generate a Factur-X invoice from a regular PDF invoice and a factur-X XML
file. The method uses a file as input (regular PDF invoice) and re-writes
Expand Down Expand Up @@ -895,6 +906,12 @@ def generate_facturx_from_file(
if not isinstance(value, (str, unicode)):
pdf_metadata[key] = ''
facturx_level = facturx_level.lower()
if zugferd_version not in ZUGFERD_VERSIONS:
logger.warning(
'Zugferd version %s is not allowed. '
'%s will be used instead', zugferd_version, ZUGFERD_VERSION[0])
zugferd_version = ZUGFERD_VERSION[0]

if facturx_level not in FACTURX_LEVEL2xsd:
if xml_root is None:
xml_root = etree.fromstring(xml_string)
Expand All @@ -917,7 +934,8 @@ def generate_facturx_from_file(
# else : generate some ?
_facturx_update_metadata_add_attachment(
new_pdf_filestream, xml_string, pdf_metadata, facturx_level,
output_intents=output_intents, additional_attachments=attachments)
output_intents=output_intents, additional_attachments=attachments,
zugferd_version=zugferd_version)
if output_pdf_file:
with open(output_pdf_file, 'wb') as output_f:
new_pdf_filestream.write(output_f)
Expand Down