diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5671a12 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2016-2017, Alexis de Lattre +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The name of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..5ac8f53 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,16 @@ +include README.rst +include requirement.txt +include facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_12.xsd +include facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_QualifiedDataType_12.xsd +include facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_15.xsd +include facturx/xsd/zugferd/ZUGFeRD1p0.xsd +include facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd +include facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd +include facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd +include facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd +include facturx/xsd/factur-x/Factur-X_EN16931.xsd +include facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd +include facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd +include facturx/xsd/factur-x/Factur-X_BASIC_WL.xsd +include facturx/xmp/ZUGFeRD_extension_schema.xmp +include facturx/xmp/Factur-X_extension_schema.xmp diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..968d691 --- /dev/null +++ b/README.rst @@ -0,0 +1,47 @@ +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 `_. + +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. + +This lib provides additionnal features such as: + +* extract the Factur-X XML file from a Factur-X PDF invoice, +* check a Factur-X XML file against the official XML Schema Definition. + +Some of the features provided by this lib also work for the ZUGFeRD e-invoicing standard (the ancestor of the Factur-X standard). + +Installation +============ + +``` +sudo pip install --upgrade factur-x +``` + +Usage +===== + +``` +from facturx import generate_facturx + +facturx_pdf_invoice = generate_facturx(regular_pdf_invoice, facturx_xml_file) +``` + +To have more examples, look at the source code of the command line tools located in the *bin* subdirectory. + +Command line tools +================== + +Several command line tools are provided with this lib: + +* facturx-pdfgen: generate a Factur-X PDF invoice from a regular PDF invoice and an XML file +* facturx-pdfextractxml: extract the Factur-X XML file from a Factur-X PDF invoice +* facturx-xmlcheck: check a Factur-X XML file against the official Factur-X XML Schema Definition + +All these commande line tools have a **-h** option that explains how to use them and shows all the available options. + +Contributors +============ + +* Alexis de Lattre diff --git a/bin/facturx-pdfextractxml b/bin/facturx-pdfextractxml new file mode 100755 index 0000000..2f2c6eb --- /dev/null +++ b/bin/facturx-pdfextractxml @@ -0,0 +1,95 @@ +#! /usr/bin/python +# -*- coding: utf-8 -*- +# © 2017 Alexis de Lattre + +from optparse import OptionParser +import sys +from facturx import get_facturx_xml_from_pdf +from facturx.facturx import logger +import logging +from os.path import isfile, isdir + +__author__ = "Alexis de Lattre " +__date__ = "August 2017" +__version__ = "0.1" + +options = [ + {'names': ('-l', '--log-level'), 'dest': 'log_level', + 'action': 'store', 'default': 'info', + 'help': "Set log level. Possible values: debug, info, warn, error. " + "Default value: info."}, + {'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 " + "(the check is enabled by default)"}, + ] + + +def main(options, arguments): + if options.log_level: + log_level = options.log_level.lower() + log_map = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warn': logging.WARN, + 'error': logging.ERROR, + } + if log_level in log_map: + logger.setLevel(log_map[log_level]) + else: + logger.error( + 'Wrong value for log level (%s). Possible values: %s', + log_level, ', '.join(log_map.keys())) + sys.exit(1) + + if len(arguments) != 2: + logger.error( + 'This command requires 2 arguments (%d used). ' + 'Use --help to get the details.', len(arguments)) + sys.exit(1) + pdf_filename = arguments[0] + out_xml_filename = arguments[1] + if not isfile(pdf_filename): + logger.error('Argument %s is not a filename', pdf_filename) + sys.exit(1) + if isdir(out_xml_filename): + logger.error( + '2nd argument %s is a directory name (should be a the ' + 'output XML filename)', out_xml_filename) + sys.exit(1) + pdf_file = open(pdf_filename) + check_xsd = True + if options.disable_xsd_check: + check_xsd = False + # The important line of code is below ! + try: + (xml_filename, xml_string) = get_facturx_xml_from_pdf( + pdf_file, check_xsd=check_xsd) + except Exception, e: + logger.error(e) + sys.exit(1) + if xml_filename and xml_string: + if isfile(out_xml_filename): + logger.warn( + 'File %s already exists. Overwriting it!', out_xml_filename) + xml_file = open(out_xml_filename, 'w') + xml_file.write(xml_string) + xml_file.close() + logger.info('File %s generated', out_xml_filename) + else: + logger.warn('File %s has not been created', out_xml_filename) + sys.exit(1) + + +if __name__ == '__main__': + usage = "Usage: facturx-pdfextractxml " + epilog = "Author: %s\n\nVersion: %s" % (__author__, __version__) + description = "This extracts the XML file from a Factur-X invoice." + parser = OptionParser(usage=usage, epilog=epilog, description=description) + for option in options: + param = option['names'] + del option['names'] + parser.add_option(*param, **option) + options, arguments = parser.parse_args() + sys.argv[:] = arguments + main(options, arguments) diff --git a/bin/facturx-pdfgen b/bin/facturx-pdfgen new file mode 100755 index 0000000..1f8e42b --- /dev/null +++ b/bin/facturx-pdfgen @@ -0,0 +1,137 @@ +#! /usr/bin/python +# -*- coding: utf-8 -*- +# © 2017 Alexis de Lattre + +from optparse import OptionParser +import sys +from facturx import generate_facturx +from facturx.facturx import logger +import logging +from os.path import isfile, isdir + +__author__ = "Alexis de Lattre " +__date__ = "August 2017" +__version__ = "0.1" + +options = [ + {'names': ('-l', '--log-level'), 'dest': 'log_level', + 'action': 'store', 'default': 'info', + 'help': "Set log level. Possible values: debug, info, warn, error. " + "Default value: info."}, + {'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 " + "(the check is enabled by default)"}, + {'names': ('-n', '--facturx-level'), 'dest': 'facturx_level', + 'action': 'store', 'default': 'autodetect', + 'help': "Specify the Factur-X level of the XML file. " + "Default: autodetect. If you specify a particular level instead of " + "using autodetection, you will win a very small amount of time " + "(less than 1 millisecond). " + "Possible values: minimum, basicwl, basic, en16931."}, + {'names': ('-a', '--meta-author'), 'dest': 'meta_author', + 'action': 'store', 'default': False, + 'help': "Specify the author for PDF metadata. Default: use the vendor " + "name extracted from the Factur-X XML file."}, + {'names': ('-k', '--meta-keywords'), 'dest': 'meta_keywords', + 'action': 'store', 'default': False, + 'help': "Specify the keywords for PDF metadata. " + "Default: 'Invoice, Factur-X'."}, + {'names': ('-t', '--meta-title'), 'dest': 'meta_title', + 'action': 'store', 'default': False, + 'help': "Specify the title of PDF metadata. " + "Default: generic English title with information extracted from " + "the Factur-X XML file such as: 'Akretion: Invoice I1242'"}, + {'names': ('-s', '--meta-subject'), 'dest': 'meta_subject', + 'action': 'store', 'default': False, + 'help': "Specify the subject of PDF metadata. " + "Default: generic English subject with information extracted from the " + "Factur-X XML file such as: " + "'Factur-X invoice I1242 dated 2017-08-17 issued by Akretion'"}, + ] + + +def main(options, arguments): + if options.log_level: + log_level = options.log_level.lower() + log_map = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warn': logging.WARN, + 'error': logging.ERROR, + } + if log_level in log_map: + logger.setLevel(log_map[log_level]) + else: + logger.error( + 'Wrong value for log level (%s). Possible values: %s', + log_level, ', '.join(log_map.keys())) + sys.exit(1) + + if len(arguments) != 3: + logger.error( + 'This command requires 3 arguments (%d used). ' + 'Use --help to get the details.', len(arguments)) + sys.exit(1) + pdf_filename = arguments[0] + xml_filename = arguments[1] + facturx_pdf_filename = arguments[2] + for filename in [pdf_filename, xml_filename]: + if not isfile(filename): + logger.error('Argument %s is not a filename', filename) + sys.exit(1) + if isdir(facturx_pdf_filename): + logger.error( + '3rd argument %s is a directory name (should be a the ' + 'Factur-X PDF filename)', facturx_pdf_filename) + sys.exit(1) + pdf_file = open(pdf_filename) + xml_file = open(xml_filename) + check_xsd = True + if options.disable_xsd_check: + check_xsd = False + pdf_metadata = None + if ( + options.meta_author or + options.meta_keywords or + options.meta_title or + options.meta_subject): + pdf_metadata = { + 'author': options.meta_author, + 'keywords': options.meta_keywords, + 'title': options.meta_title, + 'subject': options.meta_subject, + } + # The important line of code is below ! + try: + pdf_content = generate_facturx( + pdf_file, xml_file, check_xsd=check_xsd, + facturx_level=options.facturx_level, pdf_metadata=pdf_metadata) + except Exception, e: + logger.error(e) + sys.exit(1) + if isfile(facturx_pdf_filename): + logger.warn( + 'File %s already exists. Overwriting it!', facturx_pdf_filename) + facturx_pdf_file = open(facturx_pdf_filename, 'w') + facturx_pdf_file.write(pdf_content) + facturx_pdf_file.close() + + +if __name__ == '__main__': + usage = "Usage: facturx-pdfgen "\ + "\n"\ + "\nIf you use one of the --meta-* arguments, you should specify "\ + "all the meta-* arguments because the default values for "\ + "metadata only apply if none of the meta-* arguments are used." + epilog = "Author: %s\n\nVersion: %s" % (__author__, __version__) + description = "This script generate a Factur-X PDF invoice from a "\ + "regular PDF invoice and a Factur-X XML file." + parser = OptionParser(usage=usage, epilog=epilog, description=description) + for option in options: + param = option['names'] + del option['names'] + parser.add_option(*param, **option) + options, arguments = parser.parse_args() + sys.argv[:] = arguments + main(options, arguments) diff --git a/bin/facturx-xmlcheck b/bin/facturx-xmlcheck new file mode 100755 index 0000000..eebfe87 --- /dev/null +++ b/bin/facturx-xmlcheck @@ -0,0 +1,85 @@ +#! /usr/bin/python +# -*- coding: utf-8 -*- +# © 2017 Alexis de Lattre + +from optparse import OptionParser +import sys +from facturx import check_facturx_xsd +from facturx.facturx import logger +import logging +from os.path import isfile + +__author__ = "Alexis de Lattre " +__date__ = "August 2017" +__version__ = "0.1" + +options = [ + {'names': ('-l', '--log-level'), 'dest': 'log_level', + 'action': 'store', 'default': 'info', + 'help': "Set log level. Possible values: debug, info, warn, error. " + "Default value: info."}, + {'names': ('-f', '--flavor'), 'dest': 'flavor', + 'action': 'store', 'default': 'autodetect', + 'help': "Set XML flavor. " + "Possible values: factur-x, zugferd or autodetect. " + "Default value: autodetect."}, + {'names': ('-n', '--facturx-level'), 'dest': 'facturx_level', + 'action': 'store', 'default': 'autodetect', + 'help': "Specify the Factur-X level of the XML file. " + "Default: autodetect. If you specify a particular level instead of " + "using autodetection, you will win a very small amount of time " + "(less than 1 millisecond). " + "Possible values: minimum, basicwl, basic, en16931."}, + ] + + +def main(options, arguments): + if options.log_level: + log_level = options.log_level.lower() + log_map = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warn': logging.WARN, + 'error': logging.ERROR, + } + if log_level in log_map: + logger.setLevel(log_map[log_level]) + else: + logger.error( + 'Wrong value for log level (%s). Possible values: %s', + log_level, ', '.join(log_map.keys())) + sys.exit(1) + + if len(arguments) != 1: + logger.error( + 'This command requires 1 argument (%d used). ' + 'Use --help to get the details.', len(arguments)) + sys.exit(1) + xml_filename = arguments[0] + if not isfile(xml_filename): + logger.error('%s is not a filename', xml_filename) + sys.exit(1) + xml_file = open(xml_filename) + # The important line of code is below ! + try: + check_facturx_xsd( + xml_file, flavor=options.flavor, + facturx_level=options.facturx_level) + except Exception, e: + logger.error(e) + sys.exit(1) + + +if __name__ == '__main__': + usage = "Usage: facturx-xmlcheck \n" + epilog = "Author: %s\n\nVersion: %s" % (__author__, __version__) + description = "This script checks the Factur-X XML against the XML "\ + "Schema Definition." + parser = OptionParser(usage=usage, epilog=epilog, description=description) + for option in options: + param = option['names'] + del option['names'] + parser.add_option(*param, **option) + options, arguments = parser.parse_args() + sys.argv[:] = arguments + main(options, arguments) diff --git a/facturx/.factur-x.py.swp b/facturx/.factur-x.py.swp new file mode 100644 index 0000000..33a1bb5 Binary files /dev/null and b/facturx/.factur-x.py.swp differ diff --git a/facturx/__init__.py b/facturx/__init__.py new file mode 100644 index 0000000..044af62 --- /dev/null +++ b/facturx/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from facturx import generate_facturx, get_facturx_flavor, get_facturx_level, check_facturx_xsd, get_facturx_xml_from_pdf diff --git a/facturx/facturx.py b/facturx/facturx.py new file mode 100644 index 0000000..e1d70ea --- /dev/null +++ b/facturx/facturx.py @@ -0,0 +1,553 @@ +# -*- coding: utf-8 -*- +# © 2016-2017, Alexis de Lattre +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * The name of the authors may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# TODO list: +# - python3 support +# - automated tests + +from StringIO import StringIO +from lxml import etree +from tempfile import NamedTemporaryFile +from datetime import datetime +from PyPDF2 import PdfFileWriter, PdfFileReader +from PyPDF2.generic import DictionaryObject, DecodedStreamObject,\ + NameObject, createStringObject, ArrayObject +import os.path +from pkg_resources import resource_filename +import logging + +FORMAT = '%(asctime)s [%(levelname)s] %(message)s' +logging.basicConfig(format=FORMAT) +logger = logging.getLogger('factur-x') +logger.setLevel(logging.INFO) + +FACTURX_FILENAME = 'factur-x.xml' +FACTURX_LEVEL2xsd = { + 'minimum': 'Factur-X_BASIC_WL.xsd', + 'basicwl': 'Factur-X_BASIC_WL.xsd', + 'basic': 'Factur-X_EN16931.xsd', + 'en16931': 'Factur-X_EN16931.xsd', # comfort +} + + +def check_facturx_xsd( + facturx_xml, flavor='autodetect', facturx_level='autodetect'): + """ + Validate the XML file against the XSD + :param facturx_xml: the Factur-X XML + :type facturx_xml: string, file or etree object + :param flavor: possible values: 'factur-x', 'zugferd' or 'autodetect' + :type flavor: string + :param facturx_level: the level of the Factur-X XML file. Default value + is 'autodetect'. The only advantage to specifiy a particular value instead + of using the autodetection is for a small perf improvement. + Possible values: minimum, basicwl, basic, en16931. + :return: True if the XML is valid against the XSD + raise an error if it is not valid against the XSD + """ + assert facturx_xml, 'Missing facturx_xml arg' + assert isinstance(flavor, (str, unicode)), 'wrong type for flavor arg' + assert isinstance(facturx_level, (type(None), str, unicode)),\ + 'wrong type for facturx_level arg' + facturx_xml_etree = None + if isinstance(facturx_xml, str): + xml_string = facturx_xml + elif isinstance(facturx_xml, unicode): + xml_string = facturx_xml.encode('utf8') + elif isinstance(facturx_xml, type(etree.Element('pouet'))): + facturx_xml_etree = facturx_xml + xml_string = etree.tostring( + facturx_xml, pretty_print=True, encoding='UTF-8', + xml_declaration=True) + elif isinstance(facturx_xml, file): + facturx_xml.seek(0) + xml_string = facturx_xml.read() + facturx_xml.close() + + if flavor not in ('factur-x', 'facturx', 'zugferd'): # autodetect + if facturx_xml_etree is None: + try: + facturx_xml_etree = etree.fromstring(xml_string) + except Exception, e: + raise Exception( + "The XML syntax is invalid: %s." % unicode(e)) + flavor = get_facturx_flavor(facturx_xml_etree) + if flavor in ('factur-x', 'facturx'): + if facturx_level not in FACTURX_LEVEL2xsd: + if facturx_xml_etree is None: + try: + facturx_xml_etree = etree.fromstring(xml_string) + except Exception, e: + raise Exception( + "The XML syntax is invalid: %s." % unicode(e)) + facturx_level = get_facturx_level(facturx_xml_etree) + if facturx_level not in FACTURX_LEVEL2xsd: + raise ValueError( + "Wrong level '%s' for Factur-X invoice." % facturx_level) + xsd_filename = FACTURX_LEVEL2xsd[facturx_level] + xsd_file = resource_filename( + __name__, 'xsd/factur-x/%s' % xsd_filename) + elif flavor == 'zugferd': + xsd_file = resource_filename( + __name__, 'xsd/xsd-zugferd/ZUGFeRD1p0.xsd') + xsd_etree_obj = etree.parse(open(xsd_file)) + official_schema = etree.XMLSchema(xsd_etree_obj) + try: + t = etree.parse(StringIO(xml_string)) + official_schema.assertValid(t) + logger.info('Factur-X XML file successfully validated against XSD') + except Exception, e: + # if the validation of the XSD fails, we arrive here + logger.error( + "The XML file is invalid against the XML Schema Definition") + logger.error(xml_string) + raise Exception( + "The %s XML file is not valid against the official " + "XML Schema Definition. The XML file and the " + "full error have been written in the server logs. " + "Here is the error, which may give you an idea on the " + "cause of the problem: %s." % (flavor.capitalize(), unicode(e))) + return True + + +def get_facturx_xml_from_pdf(pdf_invoice, check_xsd=True): + assert pdf_invoice, 'Missing pdf_invoice arg' + assert isinstance(check_xsd, bool), 'Missing pdf_invoice arg' + if isinstance(pdf_invoice, str): + pdf_file = StringIO(pdf_invoice) + elif isinstance(pdf_invoice, file): + pdf_file = pdf_invoice + else: + raise TypeError( + "The first argument of the methodPDFisFacturXInvoice must be " + "either a string or a file (it is a %s)." % type(pdf_invoice)) + xml_string = xml_filename = False + try: + pdf = PdfFileReader(pdf_file) + pdf_root = pdf.trailer['/Root'] + logger.debug('pdf_root=%s', pdf_root) + embeddedfiles = pdf_root['/Names']['/EmbeddedFiles']['/Names'] + i = 0 + for embeddedfile in embeddedfiles[:-1]: + if embeddedfile in (FACTURX_FILENAME, 'ZUGFeRD-invoice.xml'): + xml_file_dict = embeddedfiles[i+1].getObject() + logger.debug('xml_file_dict=%s', xml_file_dict) + tmp_xml_string = xml_file_dict['/EF']['/F'].getData() + xml_root = etree.fromstring(tmp_xml_string) + logger.info( + 'A valid XML file %s has been found in the PDF file', + embeddedfile) + if check_xsd: + check_facturx_xsd(xml_root) + xml_string = tmp_xml_string + xml_filename = embeddedfile + else: + xml_string = tmp_xml_string + xml_filename = embeddedfile + break + except: + logger.error('No valid XML file found in the PDF') + return (None, None) + logger.info('Returning an XML file %s', xml_filename) + logger.debug('Content of the XML file: %s', xml_string) + return (xml_filename, xml_string) + + +def _get_pdf_timestamp(): + now_dt = datetime.now() + # example date format: "D:20141006161354+02'00'" + pdf_date = now_dt.strftime("D:%Y%m%d%H%M%S+00'00'") + return pdf_date + + +def _get_metadata_timestamp(): + now_dt = datetime.now() + # example format : 2014-07-25T14:01:22+02:00 + meta_date = now_dt.strftime('%Y-%m-%dT%H:%M:%S+00:00') + return meta_date + + +def _prepare_pdf_metadata_txt(pdf_metadata): + pdf_date = _get_pdf_timestamp() + info_dict = { + '/Author': pdf_metadata.get('author', ''), + '/CreationDate': pdf_date, + '/Creator': u'factur-x Python lib by Alexis de Lattre', + '/Keywords': pdf_metadata.get('keywords', ''), + '/ModDate': pdf_date, + '/Subject': pdf_metadata.get('subject', ''), + '/Title': pdf_metadata.get('title', ''), + } + return info_dict + + +def _prepare_pdf_metadata_xml(facturx_level, pdf_metadata): + nsmap_rdf = {'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'} + nsmap_dc = {'dc': 'http://purl.org/dc/elements/1.1/'} + nsmap_pdf = {'pdf': 'http://ns.adobe.com/pdf/1.3/'} + nsmap_xmp = {'xmp': 'http://ns.adobe.com/xap/1.0/'} + nsmap_pdfaid = {'pdfaid': 'http://www.aiim.org/pdfa/ns/id/'} + nsmap_fx = { + 'fx': 'urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#'} + ns_dc = '{%s}' % nsmap_dc['dc'] + ns_rdf = '{%s}' % nsmap_rdf['rdf'] + ns_pdf = '{%s}' % nsmap_pdf['pdf'] + ns_xmp = '{%s}' % nsmap_xmp['xmp'] + ns_pdfaid = '{%s}' % nsmap_pdfaid['pdfaid'] + ns_fx = '{%s}' % nsmap_fx['fx'] + ns_xml = '{http://www.w3.org/XML/1998/namespace}' + + root = etree.Element(ns_rdf + 'RDF', nsmap=nsmap_rdf) + desc_pdfaid = etree.SubElement( + root, ns_rdf + 'Description', nsmap=nsmap_pdfaid) + desc_pdfaid.set(ns_rdf + 'about', '') + etree.SubElement( + desc_pdfaid, ns_pdfaid + 'part').text = '3' + etree.SubElement( + desc_pdfaid, ns_pdfaid + 'conformance').text = 'B' + desc_dc = etree.SubElement( + root, ns_rdf + 'Description', nsmap=nsmap_dc) + desc_dc.set(ns_rdf + 'about', '') + dc_title = etree.SubElement(desc_dc, ns_dc + 'title') + dc_title_alt = etree.SubElement(dc_title, ns_rdf + 'Alt') + dc_title_alt_li = etree.SubElement( + dc_title_alt, ns_rdf + 'li') + dc_title_alt_li.text = pdf_metadata.get('title', '') + dc_title_alt_li.set(ns_xml + 'lang', 'x-default') + dc_creator = etree.SubElement(desc_dc, ns_dc + 'creator') + dc_creator_seq = etree.SubElement(dc_creator, ns_rdf + 'Seq') + etree.SubElement( + dc_creator_seq, ns_rdf + 'li').text = pdf_metadata.get('author', '') + dc_desc = etree.SubElement(desc_dc, ns_dc + 'description') + dc_desc_alt = etree.SubElement(dc_desc, ns_rdf + 'Alt') + dc_desc_alt_li = etree.SubElement( + dc_desc_alt, ns_rdf + 'li') + dc_desc_alt_li.text = pdf_metadata.get('subject', '') + dc_desc_alt_li.set(ns_xml + 'lang', 'x-default') + desc_adobe = etree.SubElement( + root, ns_rdf + 'Description', nsmap=nsmap_pdf) + desc_adobe.set(ns_rdf + 'about', '') + producer = etree.SubElement( + desc_adobe, ns_pdf + 'Producer') + producer.text = 'PyPDF2' + desc_xmp = etree.SubElement( + root, ns_rdf + 'Description', nsmap=nsmap_xmp) + desc_xmp.set(ns_rdf + 'about', '') + creator = etree.SubElement( + desc_xmp, ns_xmp + 'CreatorTool') + creator.text = 'factur-x python lib by Alexis de Lattre' + timestamp = _get_metadata_timestamp() + etree.SubElement(desc_xmp, ns_xmp + 'CreateDate').text = timestamp + etree.SubElement(desc_xmp, ns_xmp + 'ModifyDate').text = timestamp + + xmp_file = resource_filename( + __name__, 'xmp/Factur-X_extension_schema.xmp') + facturx_ext_schema_root = etree.parse(open(xmp_file)) + # The Factur-X extension schema must be embedded into each PDF document + facturx_ext_schema_desc_xpath = facturx_ext_schema_root.xpath( + '//rdf:Description', namespaces=nsmap_rdf) + root.append(facturx_ext_schema_desc_xpath[1]) + # Now is the Factur-X description tag + facturx_desc = etree.SubElement( + root, ns_rdf + 'Description', nsmap=nsmap_fx) + facturx_desc.set(ns_rdf + 'about', '') + facturx_desc.set(ns_fx + 'ConformanceLevel', facturx_level.upper()) + facturx_desc.set(ns_fx + 'DocumentFileName', FACTURX_FILENAME) + facturx_desc.set(ns_fx + 'DocumentType', 'INVOICE') + facturx_desc.set(ns_fx + 'Version', '1.0') + + xml_str = etree.tostring( + root, pretty_print=True, encoding="UTF-8", xml_declaration=False) + logger.debug('metadata XML:') + logger.debug(xml_str) + return xml_str + + +def _facturx_update_metadata_add_attachment( + pdf_filestream, facturx_xml_str, pdf_metadata, facturx_level): + '''This method is inspired from the code of the addAttachment() + method of the PyPDF2 lib''' + # The entry for the file + moddate = DictionaryObject() + moddate.update({ + NameObject('/ModDate'): createStringObject(_get_pdf_timestamp())}) + file_entry = DecodedStreamObject() + file_entry.setData(facturx_xml_str) + file_entry.update({ + NameObject("/Type"): NameObject("/EmbeddedFile"), + NameObject("/Params"): moddate, + # 2F is '/' in hexadecimal + NameObject("/Subtype"): NameObject("/text#2Fxml"), + }) + file_entry_obj = pdf_filestream._addObject(file_entry) + # The Filespec entry + efEntry = DictionaryObject() + efEntry.update({ + NameObject("/F"): file_entry_obj, + NameObject('/UF'): file_entry_obj, + }) + + fname_obj = createStringObject(FACTURX_FILENAME) + filespec = DictionaryObject() + filespec.update({ + NameObject("/AFRelationship"): NameObject("/Alternative"), + NameObject("/Desc"): createStringObject("Factur-X Invoice"), + NameObject("/Type"): NameObject("/Filespec"), + NameObject("/F"): fname_obj, + NameObject("/EF"): efEntry, + NameObject("/UF"): fname_obj, + }) + embeddedFilesNamesDictionary = DictionaryObject() + embeddedFilesNamesDictionary.update({ + NameObject("/Names"): ArrayObject( + [fname_obj, pdf_filestream._addObject(filespec)]) + }) + # Then create the entry for the root, as it needs a + # reference to the Filespec + embeddedFilesDictionary = DictionaryObject() + embeddedFilesDictionary.update({ + NameObject("/EmbeddedFiles"): embeddedFilesNamesDictionary + }) + # Update the root + metadata_xml_str = _prepare_pdf_metadata_xml(facturx_level, pdf_metadata) + metadata_file_entry = DecodedStreamObject() + metadata_file_entry.setData(metadata_xml_str) + metadata_value = pdf_filestream._addObject(metadata_file_entry) + af_value = pdf_filestream._addObject( + ArrayObject([pdf_filestream._addObject(filespec)])) + pdf_filestream._root_object.update({ + NameObject("/AF"): af_value, + NameObject("/Metadata"): metadata_value, + NameObject("/Names"): embeddedFilesDictionary, + }) + metadata_txt_dict = _prepare_pdf_metadata_txt(pdf_metadata) + pdf_filestream.addMetadata(metadata_txt_dict) + + +def _extract_base_info(facturx_xml_etree): + namespaces = facturx_xml_etree.nsmap + date_xpath = facturx_xml_etree.xpath( + '//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString', + namespaces=namespaces) + date = date_xpath[0].text + date_dt = datetime.strptime(date, '%Y%m%d') + inv_num_xpath = facturx_xml_etree.xpath( + '//rsm:ExchangedDocument/ram:ID', namespaces=namespaces) + inv_num = inv_num_xpath[0].text + seller_xpath = facturx_xml_etree.xpath( + '//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:Name', + namespaces=namespaces) + seller = seller_xpath[0].text + doc_type_xpath = facturx_xml_etree.xpath( + '//rsm:ExchangedDocument/ram:TypeCode', namespaces=namespaces) + doc_type = doc_type_xpath[0].text + base_info = { + 'seller': seller, + 'number': inv_num, + 'date': date_dt, + 'doc_type': doc_type, + } + logger.debug('Extraction of base_info: %s', base_info) + return base_info + + +def _base_info2pdf_metadata(base_info): + if base_info['doc_type'] == '381': + doc_type_name = u'Refund' + else: + doc_type_name = u'Invoice' + date_str = datetime.strftime(base_info['date'], '%Y-%m-%d') + title = '%s: %s %s' % ( + base_info['seller'], doc_type_name, base_info['number']) + subject = 'Factur-X %s %s dated %s issued by %s' % ( + doc_type_name, base_info['number'], date_str, base_info['seller']) + pdf_metadata = { + 'author': base_info['seller'], + 'keywords': u'%s, Factur-X' % doc_type_name, + 'title': title, + 'subject': subject, + } + logger.debug('Converted base_info to pdf_metadata: %s', pdf_metadata) + return pdf_metadata + + +def get_facturx_level(facturx_xml_etree): + assert isinstance(facturx_xml_etree, type(etree.Element('pouet'))),\ + 'facturx_xml_etree must be an etree.Element() object' + namespaces = facturx_xml_etree.nsmap + doc_id_xpath = facturx_xml_etree.xpath( + "//rsm:ExchangedDocumentContext" + "/ram:GuidelineSpecifiedDocumentContextParameter" + "/ram:ID", namespaces=namespaces) + if not doc_id_xpath: + raise ValueError( + "This XML is not a Factur-X XML because it misses the XML tag " + "ExchangedDocumentContext/" + "GuidelineSpecifiedDocumentContextParameter/ID.") + doc_id = doc_id_xpath[0].text + level = doc_id.split(':')[-1] + logger.info('Factur-X level is %s (autodetected)', level) + return level + + +def get_facturx_flavor(facturx_xml_etree): + assert isinstance(facturx_xml_etree, type(etree.Element('pouet'))),\ + 'facturx_xml_etree must be an etree.Element() object' + if facturx_xml_etree.tag.startswith('{urn:un:unece:uncefact:'): + flavor = 'factur-x' + elif facturx_xml_etree.tag.startswith('{urn:ferd:'): + flavor = 'zugferd' + else: + raise Exception( + "Could not detect if the invoice is a Factur-X or ZUGFeRD " + "invoice.") + logger.info('Factur-X flavor is %s (autodetected)', flavor) + return flavor + + +def generate_facturx( + pdf_invoice, facturx_xml, facturx_level='autodetect', + check_xsd=True, pdf_metadata=None, output_pdf_file=None): + """ + Generate a Factur-X invoice from a regular PDF invoice and a factur-X XML + file. This is the main method of this lib. + :param pdf_invoice: the regular PDF invoice + :type pdf_invoice: binary or file + :param facturx_xml: the Factur-X XML + :type facturx_xml: string, file or etree object + :param facturx_level: the level of the Factur-X XML file. Default value + is 'autodetect'. The only advantage to specifiy a particular value instead + of using the autodetection is for a very very small perf improvement. + Possible values: minimum, basicwl, basic, en16931. + :type facturx_level: string + :param check_xsd: if enable, checks the Factur-X XML file against the XSD + (XML Schema Definition). If this step has already been performed + beforehand, you should disable this feature to avoid a double check + and get a small performance improvement. + :type check_xsd: boolean + :param pdf_metadata: Specify the metadata of the generated Factur-X PDF. + If pdf_metadata is None (default value), this lib will generate some + metadata in English by extracting relevant info from the Factur-X XML. + Here is an example for the pdf_metadata argument: + pdf_metadata = { + 'author': 'Akretion', + 'keywords': 'Factur-X, Invoice', + 'title': 'Akretion: Invoice I1242', + 'subject': + 'Factur-X invoice I1242 dated 2017-08-17 issued by Akretion', + } + If you pass the pdf_metadata argument, you will not use the automatic + generation based on the extraction of the Factur-X XML file, which will + bring a very small perf improvement. + :type pdf_metadata: dict + """ + start_chrono = datetime.now() + logger.debug('1st arg pdf_invoice type=%s', type(pdf_invoice)) + logger.debug('2nd arg facturx_xml type=%s', type(facturx_xml)) + logger.debug('optional arg facturx_level=%s', facturx_level) + logger.debug('optional arg check_xsd=%s', check_xsd) + logger.debug('optional arg pdf_metadata=%s', pdf_metadata) + assert pdf_invoice, 'Missing pdf_invoice arg' + assert facturx_xml, 'Missing facturx_xml arg' + assert isinstance(facturx_level, (str, unicode)), 'Wrong facturx_level arg' + assert isinstance(check_xsd, bool), 'check_xsd arg must be a boolean' + assert isinstance(pdf_metadata, (type(None), dict)),\ + 'pdf_metadata must be a dict or None' + assert isinstance(output_pdf_file, (type(None), str)),\ + 'output_pdf_file must be a string or None' + if isinstance(pdf_invoice, str): + original_pdf_file = StringIO(pdf_invoice) + elif isinstance(pdf_invoice, file): + original_pdf_file = pdf_invoice + else: + raise TypeError( + "The first argument of the method GenerateFacturX must be " + "either a string or a file (it is a %s)." % type(pdf_invoice)) + xml_root = None + if isinstance(facturx_xml, str): + xml_string = facturx_xml + elif isinstance(facturx_xml, unicode): + xml_string = facturx_xml.encode('utf8') + elif isinstance(facturx_xml, type(etree.Element('pouet'))): + xml_root = facturx_xml + xml_string = etree.tostring( + xml_root, pretty_print=True, encoding='UTF-8', + xml_declaration=True) + elif isinstance(facturx_xml, file): + facturx_xml.seek(0) + xml_string = facturx_xml.read() + facturx_xml.close() + else: + raise TypeError( + "The second argument of the method GenerateFacturX must be " + "either a string, an etree.Element() object or a file " + "(it is a %s)." % type(facturx_xml)) + if pdf_metadata is None: + if xml_root is None: + xml_root = etree.fromstring(xml_string) + base_info = _extract_base_info(xml_root) + pdf_metadata = _base_info2pdf_metadata(base_info) + else: + # clean-up pdf_metadata dict + for key, value in pdf_metadata.iteritems(): + if not isinstance(value, (str, unicode)): + pdf_metadata[key] = '' + facturx_level = facturx_level.lower() + if facturx_level not in FACTURX_LEVEL2xsd: + if xml_root is None: + xml_root = etree.fromstring(xml_string) + logger.debug('Factur-X level will be autodetected') + facturx_level = get_facturx_level(xml_root) + if check_xsd: + check_facturx_xsd( + xml_string, flavor='factur-x', facturx_level=facturx_level) + assert isinstance(pdf_metadata, dict), 'pdf_metadata must be a dict' + original_pdf = PdfFileReader(original_pdf_file) + new_pdf_filestream = PdfFileWriter() + new_pdf_filestream.appendPagesFromReader(original_pdf) + _facturx_update_metadata_add_attachment( + new_pdf_filestream, xml_string, pdf_metadata, facturx_level) + pdf_content = True + if output_pdf_file: + if os.path.isdir(output_pdf_file): + raise ValueError( + "The argument output_pdf_file (%s) is a directory. " + "It must be a file." % output_pdf_file) + with open(output_pdf_file, 'w') as f: + new_pdf_filestream.write(f) + f.close() + else: + with NamedTemporaryFile(prefix='invoice-facturx-', suffix='.pdf') as f: + new_pdf_filestream.write(f) + f.seek(0) + pdf_content = f.read() + f.close() + logger.info('%s file added to PDF invoice', FACTURX_FILENAME) + end_chrono = datetime.now() + logger.info( + 'Factur-X invoice generated in %s seconds', + (end_chrono - start_chrono).total_seconds()) + return pdf_content diff --git a/facturx/xmp/Factur-X_extension_schema.xmp b/facturx/xmp/Factur-X_extension_schema.xmp new file mode 100644 index 0000000..0639209 --- /dev/null +++ b/facturx/xmp/Factur-X_extension_schema.xmp @@ -0,0 +1,79 @@ + + + + + + BASIC + factur-x.xml + INVOICE + 1.0 + + + + + + + + + Factur-X PDFA Extension Schema + urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# + fx + + + + DocumentFileName + Text + external + name of the embedded XML invoice file + + + DocumentType + Text + external + INVOICE + + + Version + Text + external + The actual version of the Factur-X XML schema + + + ConformanceLevel + Text + external + The conformance level of the embedded Factur-X data + + + + + + + + \ No newline at end of file diff --git a/facturx/xmp/ZUGFeRD_extension_schema.xmp b/facturx/xmp/ZUGFeRD_extension_schema.xmp new file mode 100644 index 0000000..d5e068c --- /dev/null +++ b/facturx/xmp/ZUGFeRD_extension_schema.xmp @@ -0,0 +1,99 @@ + + + + + + BASIC + ZUGFeRD-invoice.xml + INVOICE + 1.0 + + + + + + + + + ZUGFeRD PDFA Extension Schema + urn:ferd:pdfa:CrossIndustryDocument:invoice:1p0# + zf + + + + DocumentFileName + Text + external + name of the embedded XML invoice file + + + DocumentType + Text + external + INVOICE + + + Version + Text + external + The actual version of the ZUGFeRD XML schema + + + ConformanceLevel + Text + external + The conformance level of the embedded ZUGFeRD data + + + + + + + + \ No newline at end of file diff --git a/facturx/xsd/factur-x/Factur-X_BASIC_WL.xsd b/facturx/xsd/factur-x/Factur-X_BASIC_WL.xsd new file mode 100644 index 0000000..42d68ba --- /dev/null +++ b/facturx/xsd/factur-x/Factur-X_BASIC_WL.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd b/facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd new file mode 100644 index 0000000..908b8fd --- /dev/null +++ b/facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd b/facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd new file mode 100644 index 0000000..1ba7797 --- /dev/null +++ b/facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd b/facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd new file mode 100644 index 0000000..baa9c12 --- /dev/null +++ b/facturx/xsd/factur-x/Factur-X_BASIC_WL_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/facturx/xsd/factur-x/Factur-X_EN16931.xsd b/facturx/xsd/factur-x/Factur-X_EN16931.xsd new file mode 100644 index 0000000..3e79051 --- /dev/null +++ b/facturx/xsd/factur-x/Factur-X_EN16931.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd b/facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd new file mode 100644 index 0000000..09fa0d4 --- /dev/null +++ b/facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd b/facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd new file mode 100644 index 0000000..7db9240 --- /dev/null +++ b/facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd @@ -0,0 +1,320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd b/facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd new file mode 100644 index 0000000..5a20d87 --- /dev/null +++ b/facturx/xsd/factur-x/Factur-X_EN16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/facturx/xsd/zugferd/ZUGFeRD1p0.xsd b/facturx/xsd/zugferd/ZUGFeRD1p0.xsd new file mode 100644 index 0000000..cc3075a --- /dev/null +++ b/facturx/xsd/zugferd/ZUGFeRD1p0.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_QualifiedDataType_12.xsd b/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_QualifiedDataType_12.xsd new file mode 100644 index 0000000..42b66e8 --- /dev/null +++ b/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_QualifiedDataType_12.xsd @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_12.xsd b/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_12.xsd new file mode 100644 index 0000000..d207ef0 --- /dev/null +++ b/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_12.xsd @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_15.xsd b/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_15.xsd new file mode 100644 index 0000000..4967014 --- /dev/null +++ b/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_15.xsd @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/requirement.txt b/requirement.txt new file mode 100644 index 0000000..79b8b25 --- /dev/null +++ b/requirement.txt @@ -0,0 +1,2 @@ +PyPDF2 +lxml diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9ea92bc --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from setuptools import setup, find_packages + +setup( + name='factur-x', + version='0.1', + author='Alexis de Lattre', + author_email='alexis.delattre@akretion.com', + url='https://github.com/akretion/factur-x', + description='Factur-X: electronic invoicing standard for Germany & France', + long_description=open('README.rst').read(), + license='BSD', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 2.7', + 'License :: OSI Approved :: BSD License', + "Operating System :: OS Independent", + ], + keywords='e-invoice ZUGFeRD Factur-X Chorus', + packages=find_packages(), + install_requires=[r.strip() for r in + open('requirement.txt').read().splitlines()], + include_package_data=True, + scripts=[ + 'bin/facturx-pdfgen', + 'bin/facturx-pdfextractxml', + 'bin/facturx-xmlcheck'], + zip_safe=False, +)