diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..3faaba1 --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,17 @@ +# Do NOT update manually; changes here will be overwritten by Copier +_commit: 55503af +_src_path: https://github.com/akretion/copier-python-package +author_email: alexis.delattre@akretion.com +author_name: Alexis de Lattre +development_status: 5 - Production/Stable +github_repo: factur-x +licence: BSD License +package_description: 'Factur-X and Order-X: electronic invoicing and ordering standards' +package_name: factur-x +python_version: +- '3.9' +- '3.10' +- '3.11' +- '3.12' +- '3.13' +- '3.14' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7bbc9d7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +--- +name: CI + +on: + push: + branches: ["master"] + tags: ["*"] + pull_request: + +jobs: + tests: + name: "Python ${{ matrix.python-version }}" + runs-on: "ubuntu-latest" + strategy: + fail-fast: false + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + steps: + - uses: "actions/checkout@v3" + - uses: "actions/setup-python@v4" + with: + python-version: "${{ matrix.python-version }}" + allow-prereleases: true + - name: "Install dependencies" + run: | + set -xe + python -VV + python -m site + python -m pip install .[test] + - name: "Run pytest targets for ${{ matrix.python-version }}" + run: | + coverage run --source src -m pytest tests -v + coverage xml + - uses: codecov/codecov-action@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ae06a3f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +on: + release: + types: + - published + +name: release + +jobs: + pypi: + name: upload release to PyPI + runs-on: ubuntu-latest + environment: release + + permissions: + # Used to authenticate to PyPI via OIDC. + id-token: write + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: ">= 3.7" + + - name: build + run: pipx run build + + - name: publish + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 7bbc71c..26e7180 100644 --- a/.gitignore +++ b/.gitignore @@ -1,101 +1,28 @@ -# Byte-compiled / optimized / DLL files +# Global directories __pycache__/ -*.py[cod] -*$py.class -# C extensions +# Global files +*.py[cod] +*.dll *.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: *.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - -# virtualenv -.venv -venv/ -ENV/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ +*.swp + +# Root directories +/.benchmarks/ +/.cache/ +/.env/ +/.idea/ +/.mypy_cache/ +/.pytest_cache/ +/.ruff_cache/ +/.vscode/ +/backend/dist/ +/dist/ +/site/ + +# Root files +/.coverage* + +# Auto-generated during builds +/src/factur-x/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0179350 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +default_language_version: + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.4.2 + hooks: + - id: ruff + args: [--exit-non-zero-on-fix] + - id: ruff-format diff --git a/LICENSE b/LICENSE.txt similarity index 99% rename from LICENSE rename to LICENSE.txt index 1c413dc..a5e4f2f 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -1,3 +1,4 @@ + Copyright (c) 2016-2023, Alexis de Lattre All rights reserved. diff --git a/README.rst b/README.rst index 196e3a9..3febf87 100644 --- a/README.rst +++ b/README.rst @@ -36,6 +36,26 @@ The PDF file *regular_pdf_file* will be updated to Factur-X/Order-X. If you want To have more examples, look at the docstrings in the source code or look at the source code of the command line tools located in the *bin* subdirectory. +Development +============= + +Use hatch +------------- + +Install the env with all lib + +``` +hatch env create +``` + +Execute the test +------------------- + +``` +hatch run test:pytest +``` + + Command line tools ================== diff --git a/facturx/__init__.py b/facturx/__init__.py deleted file mode 100644 index c2d5da8..0000000 --- a/facturx/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -__version__ = "4.2" -from .facturx import generate_from_file, \ - generate_from_binary, \ - get_xml_namespaces, \ - get_flavor, \ - get_facturx_level, \ - get_level, \ - xml_check_xsd, \ - xml_check_schematron, \ - get_facturx_xml_from_pdf, \ - get_orderx_xml_from_pdf, \ - get_xml_from_pdf, \ - get_orderx_type diff --git a/facturx/scripts/pdfgen.py b/facturx/scripts/pdfgen.py deleted file mode 100755 index 2e9de5d..0000000 --- a/facturx/scripts/pdfgen.py +++ /dev/null @@ -1,199 +0,0 @@ -#! /usr/bin/env python -# Copyright 2017-2023 Alexis de Lattre - -import argparse -import sys -from facturx import generate_from_file, __version__ as fxversion -from facturx.facturx import logger -import logging -from os.path import isfile, isdir, basename - -__author__ = "Alexis de Lattre " -__date__ = "October 2025" -__version__ = "0.9" - - -def pdfgen(args): - logger.info('pdfgen version %s using factur-x lib version %s', __version__, fxversion) - if args.log_level: - log_level = args.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) - - pdf_filename = args.regular_pdf_file - output_pdf_filename = args.facturx_orderx_pdf_file - additional_attachment_filenames = args.optional_attachments - for filename in [pdf_filename, args.xml_file] + additional_attachment_filenames: - if not isfile(filename): - logger.error('Argument %s is not a filename', filename) - sys.exit(1) - if isdir(output_pdf_filename): - logger.error( - '3rd argument %s is a directory name (should be a the ' - 'Factur-X or Order-X PDF filename)', output_pdf_filename) - sys.exit(1) - check_xsd = not args.disable_xsd_check - check_schematron = not args.disable_schematron_check - pdf_metadata = None - if ( - args.meta_author or - args.meta_keywords or - args.meta_title or - args.meta_subject): - pdf_metadata = { - 'author': args.meta_author, - 'keywords': args.meta_keywords, - 'title': args.meta_title, - 'subject': args.meta_subject, - } - if isfile(output_pdf_filename): - if args.overwrite: - logger.warning( - 'File %s already exists. Overwriting it.', - output_pdf_filename) - else: - logger.error( - 'File %s already exists. Exit.', output_pdf_filename) - sys.exit(1) - attachments = {} - for additional_attachment_filename in additional_attachment_filenames: - attachments[basename(additional_attachment_filename)] = { - 'filepath': additional_attachment_filename} - lang = args.lang or None - xmp_compression = not args.disable_xmp_compression - try: - # The important line of code is below ! - generate_from_file( - pdf_filename, args.xml_file, - check_xsd=check_xsd, check_schematron=check_schematron, - flavor=args.flavor, level=args.level, orderx_type=args.orderx_type, - pdf_metadata=pdf_metadata, lang=lang, output_pdf_file=output_pdf_filename, - attachments=attachments, afrelationship=args.afrelationship, - xmp_compression=xmp_compression) - except Exception: - # no need to re-print the error log, it is already present in the logs - logger.error('factur-x lib call failed, exiting.') - sys.exit(1) - - -def main(args=None): - if args is None: - args = sys.argv[1:] - usage = "facturx-pdfgen "\ - " " - epilog = "Author: %s - Version: %s" % (__author__, __version__) - description = "This script generate a Factur-X or Order-X PDF from a "\ - "regular PDF/A document and a Factur-X or Order-X XML file. "\ - "It can also include additional embedded files in the PDF. "\ - "To generate a valid PDF/A-3 document as requested by the "\ - "Factur-X/Order-X standards, you need to give a valid PDF/A "\ - "document as input."\ - "\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." - parser = argparse.ArgumentParser( - usage=usage, epilog=epilog, description=description) - parser.add_argument( - '-l', '--log-level', dest='log_level', default='info', - help="Set log level. Possible values: debug, info, warn, error. " - "Default value: info.") - parser.add_argument( - '-d', '--disable-xsd-check', dest='disable_xsd_check', - action='store_true', - help="De-activate XML Schema Definition check on XML file " - "(the check is enabled by default)") - parser.add_argument( - '-ds', '--disable-schematron-check', dest='disable_schematron_check', - action='store_true', - help="De-activate Schematron check on XML file " - "(the check is enabled by default)") - parser.add_argument( - '-f', '--flavor', dest='flavor', default='autodetect', - help="Specify if you want to generate a Factur-X or Order-X PDF file. " - "Default: autodetect. If you specify a particular flavor instead of " - "using autodetection from the XML, you will win a very small amount of time " - "(less than 1 millisecond). " - "Possible values: order-x, factur-x or autodetect.") - parser.add_argument( - '-n', '--level', '--facturx-level', dest='level', default='autodetect', - help="Specify the Factur-X or Order-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 for Factur-X: minimum, basicwl, basic, en16931, extended." - "Possible values for Order-X: basic, comfort, extended." - ) - parser.add_argument( - '-p', '--orderx-type', dest='orderx_type', default='autodetect', - help="When you generate an Order-X document, specify the order type. " - "Default: autodetect. If you specify a particular order type instead of " - "using autodetection, you will win a very small amount of time " - "(less than 1 millisecond). " - "Possible values: order, order_change, order_response.") - parser.add_argument( - '-g', '--lang', dest='lang', - help="Set the language identifier as RFC 3066 to specify the " - "natural language of the PDF document. Example: en-US.") - parser.add_argument( - '-r', '--afrelationship', dest='afrelationship', default='data', - help="Set the AFRelationship PDF property of the Factur-X/Order-X XML file. " - "Possible values: data, source, alternative. " - "Default value: data.") - parser.add_argument( - '-a', '--meta-author', dest='meta_author', - help="Specify the author for PDF metadata. Default: use the vendor " - "name extracted from the XML file.") - parser.add_argument( - '-k', '--meta-keywords', dest='meta_keywords', - help="Specify the keywords for PDF metadata. " - "Default for Factur-X: 'Invoice, Factur-X'." - "Default for Order-X: 'Order Change, Order-X' where 'Order Change' is " - "the order type.") - parser.add_argument( - '-t', '--meta-title', dest='meta_title', - help="Specify the title of PDF metadata. " - "Default: generic English title with information extracted from " - "the XML file such as: 'Akretion: Invoice I1242'") - parser.add_argument( - '-s', '--meta-subject', dest='meta_subject', - help="Specify the subject of PDF metadata. " - "Default: generic English subject with information extracted from the " - "XML file such as: " - "'Factur-X invoice I1242 dated 2017-08-17 issued by Akretion'") - parser.add_argument( - '-nz', '--disable-xmp-compression', dest="disable_xmp_compression", - action='store_true', help="Disable flate compression of the XMP metadata " - "(compression is enabled by default). You should disable compression of " - "the XMP metadata if you plan to later add a PAdES signature " - "on the generated PDF file.") - parser.add_argument( - '-w', '--overwrite', dest='overwrite', action='store_true', - help="Overwrite output PDF file if it already exists.") - parser.add_argument("regular_pdf_file", help="Regular PDF invoice") - parser.add_argument("xml_file", help="Factur-X or Order-X XML file") - parser.add_argument( - "facturx_orderx_pdf_file", help="Generated Factur-X or Order-X PDF file") - parser.add_argument( - "optional_attachments", nargs='*', - help="Optional list of additionnal attachments") - args = parser.parse_args() - pdfgen(args) - - -def run(): - if __name__ == '__main__': - main() - - -run() diff --git a/pyproject.toml b/pyproject.toml index 290131a..f24d956 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,37 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + [project] name = "factur-x" description = "Factur-X and Order-X: electronic invoicing and ordering standards" authors = [{name = "Alexis de Lattre", email = "alexis.delattre@akretion.com"}] -readme = "README.rst" classifiers = [ - "Programming Language :: Python", "Topic :: Office/Business :: Financial :: Accounting", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", ] +readme = "README.rst" keywords = ["e-invoice", "ZUGFeRD", "Factur-X", "Order-X", "e-procurement"] -license = {file = "LICENSE"} -requires-python = ">=3.7" -dynamic = ["dependencies", "version"] +license = {file = "LICENSE.txt"} +dependencies = [ + "pypdf>=5.3.0", + "lxml", + "saxonche", +] +requires-python = ">=3.9" +dynamic = ["version"] -# Needed if we enable facturx-webservice -# [project.optional-dependencies] -# web = [ -# "flask" -# ] +[project.optional-dependencies] +test = [ + "pytest", + "coverage[toml]", +] [project.urls] Homepage = "https://github.com/akretion/factur-x" @@ -34,15 +45,64 @@ facturx-xmlcheck = "facturx.scripts.xmlcheck:main" # I consider the webservice scripts as a proof of concept, not real clean code # facturx-webservice = "facturx.scripts.webservice:main" -[build-system] -requires = ["hatchling", "hatch-requirements-txt"] -build-backend = "hatchling.build" +[tool.hatch.version] +source = "vcs" -[tool.hatch.metadata.hooks.requirements_txt] -files = ["requirements.txt"] +[tool.hatch.build] +exclude = [ + "/.github", +] [tool.hatch.build.targets.wheel] -packages = ["facturx"] +packages = ["src/facturx"] -[tool.hatch.version] -path = "facturx/__init__.py" +[tool.hatch.envs.test] +features = [ + "test" +] + +[tool.ruff] +target-version = "py39" +fix = true + +[tool.ruff.lint] +select = [ + "B", + "E", + "F", + "I", + "UP", + "W", +] +ignore = [ +] +exclude = [ + "docs/conf.py", +] + +[tool.ruff.lint.isort] +known-first-party = [] + + +[tool.coverage.run] +branch = true +source_pkgs = ["factur-x"] + +[tool.coverage.paths] +source = ["src", ".tox/*/site-packages"] + +[tool.coverage.report] +show_missing = true +exclude_lines = [ + "pragma: no cover", +] + + + +[tool.towncrier] +package = "factur-x" +package_dir = "src" +filename = "HISTORY.rst" +directory = "news" +issue_format = "`#{issue} `_" +title_format = "{version} ({project_date})" diff --git a/src/facturx/__init__.py b/src/facturx/__init__.py new file mode 100644 index 0000000..00752dd --- /dev/null +++ b/src/facturx/__init__.py @@ -0,0 +1,30 @@ +__version__ = "4.2" +from .facturx import ( + generate_from_binary, + generate_from_file, + get_facturx_level, + get_facturx_xml_from_pdf, + get_flavor, + get_level, + get_orderx_type, + get_orderx_xml_from_pdf, + get_xml_from_pdf, + get_xml_namespaces, + xml_check_schematron, + xml_check_xsd, +) + +__all__ = [ + "generate_from_binary", + "generate_from_file", + "get_facturx_level", + "get_facturx_xml_from_pdf", + "get_flavor", + "get_level", + "get_orderx_type", + "get_orderx_xml_from_pdf", + "get_xml_from_pdf", + "get_xml_namespaces", + "xml_check_schematron", + "xml_check_xsd", +] diff --git a/facturx/facturx.py b/src/facturx/facturx.py similarity index 58% rename from facturx/facturx.py rename to src/facturx/facturx.py index bac4d6d..c69809e 100644 --- a/facturx/facturx.py +++ b/src/facturx/facturx.py @@ -27,89 +27,106 @@ # - add automated tests (currently, we only have tests at odoo module level) # - keep original metadata by copy of pdf_tailer[/Info] ? +import importlib.resources as importlib_resources +from datetime import datetime from io import BytesIO, IOBase -from lxml import etree from tempfile import NamedTemporaryFile -from datetime import datetime -from pypdf import PdfWriter, PdfReader -from pypdf.generic import DictionaryObject, DecodedStreamObject, \ - NameObject, NumberObject, ArrayObject, create_string_object, ByteStringObject + import saxonche -import importlib.resources as importlib_resources +from lxml import etree +from pypdf import PdfReader, PdfWriter +from pypdf.generic import ( + ArrayObject, + ByteStringObject, + DecodedStreamObject, + DictionaryObject, + NameObject, + NumberObject, + create_string_object, +) + try: - importlib_resources.files # added in py3.9 + _ = importlib_resources.files # added in py3.9 except AttributeError: import importlib_resources # py3.8 compat: pip install importlib-resources -import importlib.metadata -import os.path -import mimetypes import hashlib +import importlib.metadata import logging - +import mimetypes +import os.path VERSION = importlib.metadata.version("factur-x") -FORMAT = '%(asctime)s [%(levelname)s] %(message)s' +FORMAT = "%(asctime)s [%(levelname)s] %(message)s" logging.basicConfig(format=FORMAT) -logger = logging.getLogger('factur-x') +logger = logging.getLogger("factur-x") logger.setLevel(logging.INFO) -FACTURX_FILENAME = 'factur-x.xml' -ZUGFERD_FILENAMES = ['zugferd-invoice.xml', 'ZUGFeRD-invoice.xml'] -ORDERX_FILENAME = 'order-x.xml' +FACTURX_FILENAME = "factur-x.xml" +ZUGFERD_FILENAMES = ["zugferd-invoice.xml", "ZUGFeRD-invoice.xml"] +ORDERX_FILENAME = "order-x.xml" ALL_FILENAMES = [FACTURX_FILENAME] + ZUGFERD_FILENAMES + [ORDERX_FILENAME] FACTURX_LEVEL2xsd = { - 'minimum': 'facturx-minimum/Factur-X_1.08_MINIMUM.xsd', - 'basicwl': 'facturx-basicwl/Factur-X_1.08_BASICWL.xsd', - 'basic': 'facturx-basic/Factur-X_1.08_BASIC.xsd', - 'en16931': 'facturx-en16931/Factur-X_1.08_EN16931.xsd', - 'extended': 'facturx-extended/Factur-X_1.08_EXTENDED.xsd', + "minimum": "facturx-minimum/Factur-X_1.08_MINIMUM.xsd", + "basicwl": "facturx-basicwl/Factur-X_1.08_BASICWL.xsd", + "basic": "facturx-basic/Factur-X_1.08_BASIC.xsd", + "en16931": "facturx-en16931/Factur-X_1.08_EN16931.xsd", + "extended": "facturx-extended/Factur-X_1.08_EXTENDED.xsd", } ORDERX_LEVEL2xsd = { - 'basic': 'orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B.xsd', - 'comfort': 'orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B.xsd', - 'extended': 'orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B.xsd', - } + "basic": "orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B.xsd", + "comfort": "orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B.xsd", + "extended": "orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B.xsd", +} FACTURX_LEVEL2xmp = { - 'minimum': 'MINIMUM', - 'basicwl': 'BASIC WL', - 'basic': 'BASIC', - 'en16931': 'EN 16931', - 'extended': 'EXTENDED', - } -ORDERX_TYPES = ('order', 'order_change', 'order_response') + "minimum": "MINIMUM", + "basicwl": "BASIC WL", + "basic": "BASIC", + "en16931": "EN 16931", + "extended": "EXTENDED", +} +ORDERX_TYPES = ("order", "order_change", "order_response") ORDERX_code2type = { - '220': 'order', - '230': 'order_change', - '231': 'order_response', - } -XML_AFRelationship = ('data', 'source', 'alternative') -ATTACHMENTS_AFRelationship = ('supplement', 'unspecified') + "220": "order", + "230": "order_change", + "231": "order_response", +} +XML_AFRelationship = ("data", "source", "alternative") +ATTACHMENTS_AFRelationship = ("supplement", "unspecified") XML_NAMESPACES = { - 'factur-x': { - 'qdt': 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100', - 'ram': 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', - 'rsm': 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100', - 'udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100', - 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + "factur-x": { + "qdt": "urn:un:unece:uncefact:data:standard:QualifiedDataType:100", + "ram": ( + "urn:un:unece:uncefact:data:standard:" + "ReusableAggregateBusinessInformationEntity:100" + ), + "rsm": "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100", + "udt": "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100", + "xsi": "http://www.w3.org/2001/XMLSchema-instance", }, - 'order-x': { - 'qdt': 'urn:un:unece:uncefact:data:standard:QualifiedDataType:128', - 'ram': 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:128', - 'rsm': 'urn:un:unece:uncefact:data:SCRDMCCBDACIOMessageStructure:100', - 'udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:128', - 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', - }, - 'zugferd': { - 'ram': 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12', - 'rsm': 'urn:ferd:CrossIndustryDocument:invoice:1p0', - 'udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:15', - 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + "order-x": { + "qdt": "urn:un:unece:uncefact:data:standard:QualifiedDataType:128", + "ram": ( + "urn:un:unece:uncefact:data:standard:" + "ReusableAggregateBusinessInformationEntity:128" + ), + "rsm": "urn:un:unece:uncefact:data:SCRDMCCBDACIOMessageStructure:100", + "udt": "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:128", + "xsi": "http://www.w3.org/2001/XMLSchema-instance", + }, + "zugferd": { + "ram": ( + "urn:un:unece:uncefact:data:standard:" + "ReusableAggregateBusinessInformationEntity:12" + ), + "rsm": "urn:ferd:CrossIndustryDocument:invoice:1p0", + "udt": "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:15", + "xsi": "http://www.w3.org/2001/XMLSchema-instance", }, } -CREATOR = f'factur-x Python lib v{VERSION} by Alexis de Lattre' +CREATOR = f"factur-x Python lib v{VERSION} by Alexis de Lattre" -def xml_check_xsd(xml, flavor='autodetect', level='autodetect'): +def xml_check_xsd(xml, flavor="autodetect", level="autodetect"): """ Validate the XML file against the XSD :param xml: the Factur-X or Order-X XML @@ -125,95 +142,91 @@ def xml_check_xsd(xml, flavor='autodetect', level='autodetect'): :return: True if the XML is valid against the XSD raise an error if it is not valid against the XSD """ - logger.debug( - 'xml_check_xsd with factur-x lib %s', VERSION) + logger.debug("xml_check_xsd with factur-x lib %s", VERSION) if not isinstance(flavor, str): - raise ValueError('Wrong type for flavor argument') + raise ValueError("Wrong type for flavor argument") if not isinstance(level, (type(None), str)): - raise ValueError('Wrong type for level argument') + raise ValueError("Wrong type for level argument") start_chrono = datetime.now() xml_etree = None if isinstance(xml, bytes): xml_bytes = xml elif isinstance(xml, str): - xml_bytes = xml.encode('utf8') - elif isinstance(xml, type(etree.Element('pouet'))): + xml_bytes = xml.encode("utf8") + elif isinstance(xml, type(etree.Element("pouet"))): xml_etree = xml xml_bytes = etree.tostring( - xml, pretty_print=True, encoding='UTF-8', - xml_declaration=True) + xml, pretty_print=True, encoding="UTF-8", xml_declaration=True + ) elif isinstance(xml, IOBase): xml.seek(0) xml_bytes = xml.read() xml.close() else: - raise ValueError('Wrong type for xml argument') + raise ValueError("Wrong type for xml argument") if not xml_bytes: - raise ValueError('xml argument is empty') + raise ValueError("xml argument is empty") # autodetect - if flavor not in ('factur-x', 'facturx', 'zugferd', 'order-x', 'orderx'): + if flavor not in ("factur-x", "facturx", "zugferd", "order-x", "orderx"): if xml_etree is None: try: xml_etree = etree.fromstring(xml_bytes) except Exception as e: - raise Exception( - "The XML syntax is invalid: %s." % str(e)) + raise Exception(f"The XML syntax is invalid: {e}.") from e flavor = get_flavor(xml_etree) - if flavor in ('factur-x', 'facturx'): + if flavor in ("factur-x", "facturx"): if level not in FACTURX_LEVEL2xsd: if xml_etree is None: try: xml_etree = etree.fromstring(xml_bytes) except Exception as e: - raise Exception( - "The XML syntax is invalid: %s." % str(e)) + raise Exception(f"The XML syntax is invalid: {e}.") from e level = get_level(xml_etree, flavor) if level not in FACTURX_LEVEL2xsd: - raise ValueError( - "Wrong level '%s' for Factur-X invoice." % level) - xsd_file = 'xsd/%s' % FACTURX_LEVEL2xsd[level] - elif flavor == 'zugferd': - xsd_file = 'xsd/zugferd/ZUGFeRD1p0.xsd' - elif flavor in ('order-x', 'orderx'): + raise ValueError(f"Wrong level '{level}' for Factur-X invoice.") + xsd_file = f"xsd/{FACTURX_LEVEL2xsd[level]}" + elif flavor == "zugferd": + xsd_file = "xsd/zugferd/ZUGFeRD1p0.xsd" + elif flavor in ("order-x", "orderx"): if level not in ORDERX_LEVEL2xsd: if xml_etree is None: try: xml_etree = etree.fromstring(xml_bytes) except Exception as e: - raise Exception( - "The XML syntax is invalid: %s." % str(e)) + raise Exception(f"The XML syntax is invalid: {e}.") from e level = get_level(xml_etree, flavor) if level not in ORDERX_LEVEL2xsd: - raise ValueError( - "Wrong level '%s' for Order-X document." % level) - xsd_file = 'xsd/%s' % ORDERX_LEVEL2xsd[level] + raise ValueError(f"Wrong level '{level}' for Order-X document.") + xsd_file = f"xsd/{ORDERX_LEVEL2xsd[level]}" xsd_absolute_filepath = importlib_resources.files(__package__).joinpath(xsd_file) - logger.debug('Using XSD file %s', xsd_absolute_filepath) - official_schema = etree.XMLSchema(file=xsd_absolute_filepath) + logger.debug("Using XSD file %s", xsd_absolute_filepath) + # str is added to be compatible with lxml 4.6.5 + official_schema = etree.XMLSchema(file=str(xsd_absolute_filepath)) try: t = etree.parse(BytesIO(xml_bytes)) official_schema.assertValid(t) except Exception as 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('XSD Error: %s', e) + logger.error("The XML file is invalid against the XML Schema Definition") + logger.error("XSD Error: %s", e) raise Exception( - "The %s XML file is not valid against the official " - "XML Schema Definition. " - "Here is the error, which may give you an idea on the " - "cause of the problem: %s." % (flavor.capitalize(), str(e))) + f"The {flavor.capitalize()} XML file is not valid against the official " + f"XML Schema Definition. Here is the error, which may give you an idea on " + f"the cause of the problem: {e}." + ) from e end_chrono = datetime.now() logger.info( - '%s XML file successfully validated against XSD in %s sec', - flavor, (end_chrono - start_chrono).total_seconds()) + "%s XML file successfully validated against XSD in %s sec", + flavor, + (end_chrono - start_chrono).total_seconds(), + ) return True -def xml_check_schematron(xml, flavor='autodetect', level='autodetect'): +def xml_check_schematron(xml, flavor="autodetect", level="autodetect"): """ Validate the XML file against the schematron :param xml: the Factur-X or Order-X XML @@ -229,80 +242,74 @@ def xml_check_schematron(xml, flavor='autodetect', level='autodetect'): :return: True if the XML is valid against the schematron raise an error if it is not valid against the schematron """ - logger.debug( - 'xml_check_schematron with factur-x lib %s', VERSION) + logger.debug("xml_check_schematron with factur-x lib %s", VERSION) if not isinstance(flavor, str): - raise ValueError('Wrong type for flavor argument') + raise ValueError("Wrong type for flavor argument") if not isinstance(level, (type(None), str)): - raise ValueError('Wrong type for level argument') + raise ValueError("Wrong type for level argument") start_chrono = datetime.now() xml_etree = None if isinstance(xml, bytes): xml_bytes = xml - xml_str = xml_bytes.decode('utf-8') + xml_str = xml_bytes.decode("utf-8") elif isinstance(xml, str): xml_str = xml - xml_bytes = xml_str.encode('utf-8') - elif isinstance(xml, type(etree.Element('pouet'))): + xml_bytes = xml_str.encode("utf-8") + elif isinstance(xml, type(etree.Element("pouet"))): xml_etree = xml xml_bytes = etree.tostring( - xml, pretty_print=True, encoding='UTF-8', - xml_declaration=True) - xml_str = xml_bytes.decode('utf-8') + xml, pretty_print=True, encoding="UTF-8", xml_declaration=True + ) + xml_str = xml_bytes.decode("utf-8") elif isinstance(xml, IOBase): xml.seek(0) xml_bytes = xml.read() xml.close() - xml_str = xml_bytes.decode('utf-8') + xml_str = xml_bytes.decode("utf-8") else: - raise ValueError('Wrong type for xml argument') + raise ValueError("Wrong type for xml argument") if not xml_str or not xml_bytes: - raise ValueError('xml argument is empty') + raise ValueError("xml argument is empty") # autodetect - if flavor not in ('factur-x', 'facturx', 'zugferd', 'order-x', 'orderx'): + if flavor not in ("factur-x", "facturx", "zugferd", "order-x", "orderx"): if xml_etree is None: try: xml_etree = etree.fromstring(xml_bytes) except Exception as e: - raise Exception( - f"The XML syntax is invalid: {str(e)}.") + raise Exception(f"The XML syntax is invalid: {e}.") from e flavor = get_flavor(xml_etree) - if flavor in ('factur-x', 'facturx'): + if flavor in ("factur-x", "facturx"): if level not in FACTURX_LEVEL2xsd: if xml_etree is None: try: xml_etree = etree.fromstring(xml_bytes) except Exception as e: - raise Exception( - f"The XML syntax is invalid: {str(e)}.") + raise Exception(f"The XML syntax is invalid: {e}.") from e level = get_level(xml_etree, flavor) if level not in FACTURX_LEVEL2xsd: - raise ValueError( - f"Wrong level '{level}' for Factur-X invoice.") + raise ValueError(f"Wrong level '{level}' for Factur-X invoice.") xsd_filename = FACTURX_LEVEL2xsd[level] - elif flavor in ('order-x', 'orderx'): + elif flavor in ("order-x", "orderx"): if level not in ORDERX_LEVEL2xsd: if xml_etree is None: try: xml_etree = etree.fromstring(xml_bytes) except Exception as e: - raise Exception( - f"The XML syntax is invalid: {str(e)}.") + raise Exception(f"The XML syntax is invalid: {e}.") from e level = get_level(xml_etree, flavor) if level not in ORDERX_LEVEL2xsd: - raise ValueError( - f"xsd/{ORDERX_LEVEL2xsd[level][:-4]}-compiled-saxonc.xsl") + raise ValueError(f"Wrong level '{level}' for Order-X document.") xsd_filename = ORDERX_LEVEL2xsd[level] else: - logger.warning('There is no schematron check for flavor %s', flavor) + logger.warning("There is no schematron check for flavor %s", flavor) return True relative_xsl_file = f"xsd/{xsd_filename[:-4]}-compiled.xsl" xsl_file = str(importlib_resources.files(__package__).joinpath(relative_xsl_file)) - logger.debug('Using schematron XSL file %s', xsl_file) - xml_str_no_bom = xml_str.lstrip('\ufeff') + logger.debug("Using schematron XSL file %s", xsl_file) + xml_str_no_bom = xml_str.lstrip("\ufeff") errors = [] with saxonche.PySaxonProcessor() as saxproc: xslt_proc = saxproc.new_xslt30_processor() @@ -312,42 +319,52 @@ def xml_check_schematron(xml, flavor='autodetect', level='autodetect'): # stylesheet export files can then be used by saxon HE executable = xslt_proc.compile_stylesheet(stylesheet_file=xsl_file) result_str = executable.transform_to_string(xdm_node=xdm_node) - logger.debug('schematron result_str=%s', result_str) + logger.debug("schematron result_str=%s", result_str) try: - svrl_root = etree.fromstring(result_str.encode('utf-8')) + svrl_root = etree.fromstring(result_str.encode("utf-8")) except Exception as e: - logger.error(f"Schematron check generated an invalid XML output. Error: {str(e)}") - logger.info('Unable to validate %s XML file against schematron', flavor) + logger.error( + f"Schematron check generated an invalid XML output. Error: {str(e)}" + ) + logger.info("Unable to validate %s XML file against schematron", flavor) return False xpath_errors = svrl_root.xpath( - ".//svrl:successful-report | .//svrl:failed-assert", namespaces=svrl_root.nsmap) + ".//svrl:successful-report | .//svrl:failed-assert", namespaces=svrl_root.nsmap + ) error_nr = 1 for xpath_error in xpath_errors: - detail_xpath = xpath_error.xpath("*[local-name() = 'text']", namespaces=svrl_root.nsmap) + detail_xpath = xpath_error.xpath( + "*[local-name() = 'text']", namespaces=svrl_root.nsmap + ) if detail_xpath: error_msg = detail_xpath[0].text and detail_xpath[0].text.strip() - error_msg = f'{error_nr}. {error_msg}' - location = xpath_error.attrib and xpath_error.attrib.get('location') + error_msg = f"{error_nr}. {error_msg}" + location = xpath_error.attrib and xpath_error.attrib.get("location") if location: - error_msg = f'{error_msg}\nError location: {location}' + error_msg = f"{error_msg}\nError location: {location}" errors.append(error_msg) error_nr += 1 if errors: logger.error( - "The XML file is invalid against the schematron: %d errors found.", len(errors)) + "The XML file is invalid against the schematron: %d errors found.", + len(errors), + ) for error_msg in errors: logger.error(error_msg) - error_list_str = '\n'.join(errors) + error_list_str = "\n".join(errors) full_error = ( f"The Factur-X XML file is not valid against the official " - f"schematron. {len(errors)} errors found:\n{error_list_str}") + f"schematron. {len(errors)} errors found:\n{error_list_str}" + ) raise Exception(full_error) end_chrono = datetime.now() logger.info( - '%s XML file successfully validated against schematron in %s sec', - flavor, (end_chrono - start_chrono).total_seconds()) + "%s XML file successfully validated against schematron in %s sec", + flavor, + (end_chrono - start_chrono).total_seconds(), + ) return True @@ -357,7 +374,8 @@ def get_facturx_xml_from_pdf(pdf_file, check_xsd=True, check_schematron=True): pdf_file, check_xsd=check_xsd, check_schematron=check_schematron, - filenames=filenames) + filenames=filenames, + ) def get_orderx_xml_from_pdf(pdf_file, check_xsd=True, check_schematron=True): @@ -366,62 +384,68 @@ def get_orderx_xml_from_pdf(pdf_file, check_xsd=True, check_schematron=True): pdf_file, check_xsd=check_xsd, check_schematron=check_schematron, - filenames=filenames) + filenames=filenames, + ) -def get_xml_from_pdf(pdf_file, check_xsd=True, check_schematron=True, filenames=[]): - logger.debug( - 'get_xml_from_pdf with factur-x lib %s', VERSION) +def get_xml_from_pdf(pdf_file, check_xsd=True, check_schematron=True, filenames=None): + logger.debug("get_xml_from_pdf with factur-x lib %s", VERSION) + if filenames is None: + filenames = [] if not pdf_file: - raise ValueError('Missing pdf_invoice argument') + raise ValueError("Missing pdf_invoice argument") if not isinstance(check_xsd, bool): - raise ValueError('Bad type for check_xsd argument') + raise ValueError("Bad type for check_xsd argument") if not isinstance(check_schematron, bool): - raise ValueError('Bad type for check_schematron argument') + raise ValueError("Bad type for check_schematron argument") if not isinstance(filenames, list): - raise ValueError('Bad type for filenames argument') + raise ValueError("Bad type for filenames argument") if isinstance(pdf_file, (str, bytes)): pdf_file_in = BytesIO(pdf_file) elif isinstance(pdf_file, IOBase): pdf_file_in = pdf_file else: raise TypeError( - "The first argument of the method get_xml_from_pdf must " - "be either a byte or a file (it is a %s)." % type(pdf_file)) + f"The first argument of the method get_xml_from_pdf must be either a byte " + f"or a file (it is a {type(pdf_file)})." + ) if not filenames: filenames = ALL_FILENAMES - logger.debug('Searching for filenames %s', filenames) + logger.debug("Searching for filenames %s", filenames) xml_bytes = xml_filename = False pdf_reader = PdfReader(pdf_file_in) for attach_obj in pdf_reader.attachment_list: filename = attach_obj.name - logger.debug('Found filename=%s', filename) - if filename.lower().endswith('.xml') and attach_obj.content: + logger.debug("Found filename=%s", filename) + if filename.lower().endswith(".xml") and attach_obj.content: try: xml_root = etree.fromstring(attach_obj.content) - logger.info( - 'A valid XML file %s has been found in the PDF', - filename) + logger.info("A valid XML file %s has been found in the PDF", filename) except Exception as e: - logger.warning( - 'File %s is not a valid XML file: %s', filename, str(e)) + logger.warning("File %s is not a valid XML file: %s", filename, str(e)) continue try: flavor = get_flavor(xml_root) except Exception as e: logger.warning( - "File %s is not a factur-x/order-x/zugferd/xrechnung file. Error: %s", - filename, e) + "File %s is not a factur-x/order-x/zugferd/xrechnung file. " + "Error: %s", + filename, + e, + ) continue - if ( - (filename == ORDERX_FILENAME and flavor != 'order-x') or - (filename == FACTURX_FILENAME and flavor != "factur-x")): + if (filename == ORDERX_FILENAME and flavor != "order-x") or ( + filename == FACTURX_FILENAME and flavor != "factur-x" + ): # Don't do that when filename is zugferd-invoice.xml # because it can be either zugferd (ie zugferd 1.0) # or 'factur-x' i.e. zugferd 2.0, see bug #41 logger.warning( "Filename is %s but detected flavor is %s. " - "This is very weird: skipping file.", filename, flavor) + "This is very weird: skipping file.", + filename, + flavor, + ) continue level = False if check_xsd or check_schematron: @@ -429,32 +453,36 @@ def get_xml_from_pdf(pdf_file, check_xsd=True, check_schematron=True, filenames= level = get_level(xml_root, flavor) except Exception: logger.warning( - 'Skipping file %s because the level could not be identified', - filename) + "Skipping file %s because the level could not be identified", + filename, + ) continue if check_xsd and level: try: xml_check_xsd(xml_root, flavor=flavor, level=level) except Exception: logger.warning( - 'Skipping file %s because it is not valid against the XSD', - filename) + "Skipping file %s because it is not valid against the XSD", + filename, + ) continue if check_schematron and level: try: xml_check_schematron(xml_root, flavor=flavor, level=level) except Exception: logger.warning( - 'Skipping file %s because it is not valid against the schematron', - filename) + "Skipping %s: not valid against the schematron", filename + ) continue xml_bytes = attach_obj.content xml_filename = filename - logger.info('XML file %s extracted from PDF', xml_filename) - logger.debug('Content of the XML file: %s', xml_bytes) + logger.info("XML file %s extracted from PDF", xml_filename) + logger.debug("Content of the XML file: %s", xml_bytes) break if not xml_filename: - logger.warning("No valid factur-x/order-x/zugferd/xrechnung XML file found in this PDF") + logger.warning( + "No valid factur-x/order-x/zugferd/xrechnung XML file found in this PDF" + ) return (xml_filename, xml_bytes) @@ -469,21 +497,21 @@ def _get_pdf_timestamp(date=None): 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') + 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': CREATOR, - '/Keywords': pdf_metadata.get('keywords', ''), - '/ModDate': pdf_date, - '/Subject': pdf_metadata.get('subject', ''), - '/Title': pdf_metadata.get('title', ''), - } + "/Author": pdf_metadata.get("author", ""), + "/CreationDate": pdf_date, + "/Creator": CREATOR, + "/Keywords": pdf_metadata.get("keywords", ""), + "/ModDate": pdf_date, + "/Subject": pdf_metadata.get("subject", ""), + "/Title": pdf_metadata.get("title", ""), + } return info_dict @@ -571,39 +599,45 @@ def _prepare_pdf_metadata_xml(flavor, level, orderx_type, pdf_metadata): """ # noqa: E501 tail = """""" key2value = { - "title": pdf_metadata.get('title', ''), - "author": pdf_metadata.get('author', ''), - "subject": pdf_metadata.get('subject', ''), - "producer": 'pypdf', + "title": pdf_metadata.get("title", ""), + "author": pdf_metadata.get("author", ""), + "subject": pdf_metadata.get("subject", ""), + "producer": "pypdf", "creator_tool": CREATOR, "timestamp": _get_metadata_timestamp(), - "version": '1.0', - } + "version": "1.0", + } - if flavor == 'order-x': - key2value.update({ - "documenttype": orderx_type.upper(), - "xml_filename": ORDERX_FILENAME, - "xmp_level": level.upper(), - }) - urn = 'urn:factur-x:pdfa:CrossIndustryDocument:1p0#' + if flavor == "order-x": + key2value.update( + { + "documenttype": orderx_type.upper(), + "xml_filename": ORDERX_FILENAME, + "xmp_level": level.upper(), + } + ) + urn = "urn:factur-x:pdfa:CrossIndustryDocument:1p0#" else: - key2value.update({ - "documenttype": 'INVOICE', - "xml_filename": FACTURX_FILENAME, - "xmp_level": FACTURX_LEVEL2xmp[level], - }) - urn = 'urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#' + key2value.update( + { + "documenttype": "INVOICE", + "xml_filename": FACTURX_FILENAME, + "xmp_level": FACTURX_LEVEL2xmp[level], + } + ) + urn = "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#" xml_str = xml_str.format(urn=urn) xml_root = etree.fromstring(xml_str) namespaces = xml_root.nsmap - namespaces.update({ - 'rdf': "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - "dc": "http://purl.org/dc/elements/1.1/", - "fx": "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#", - "pdf": "http://ns.adobe.com/pdf/1.3/", - "xmp": "http://ns.adobe.com/xap/1.0/", - }) + namespaces.update( + { + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "dc": "http://purl.org/dc/elements/1.1/", + "fx": "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#", + "pdf": "http://ns.adobe.com/pdf/1.3/", + "xmp": "http://ns.adobe.com/xap/1.0/", + } + ) xpath2key = { "/x:xmpmeta/rdf:RDF/rdf:Description/dc:title//rdf:li": "title", "/x:xmpmeta/rdf:RDF/rdf:Description/dc:creator//rdf:li": "author", @@ -616,153 +650,195 @@ def _prepare_pdf_metadata_xml(flavor, level, orderx_type, pdf_metadata): "/x:xmpmeta/rdf:RDF/rdf:Description/fx:DocumentFileName": "xml_filename", "/x:xmpmeta/rdf:RDF/rdf:Description/fx:Version": "version", "/x:xmpmeta/rdf:RDF/rdf:Description/fx:ConformanceLevel": "xmp_level", - } + } for xpath, key in xpath2key.items(): xml_nodes = xml_root.xpath(xpath, namespaces=namespaces) if len(xml_nodes) != 1: raise Exception( - f"XMP generation: wrong xpath {xpath} for {key}. Please report it as a bug.") + f"XMP generation: wrong xpath {xpath} for {key}. " + f"Please report it as a bug." + ) xml_node = xml_nodes[0] expected_node_text = f"##{key}" if xml_node.text != expected_node_text: raise Exception( f"XMP generation: xpath {xpath} contains {xml_node.text} " - f"instead of {expected_node_text}. Please report it as a bug.") + f"instead of {expected_node_text}. Please report it as a bug." + ) value = key2value[key] xml_node.text = value xml_bytes = etree.tostring(xml_root) - xml_str_final = "\n".join([head, xml_bytes.decode('utf-8'), tail]) - xml_bytes_final = xml_str_final.encode('utf-8') - logger.debug('metadata XML:') + xml_str_final = "\n".join([head, xml_bytes.decode("utf-8"), tail]) + xml_bytes_final = xml_str_final.encode("utf-8") + logger.debug("metadata XML:") logger.debug(xml_bytes_final) return xml_bytes_final def _filespec_additional_attachments( - pdf_writer, name_arrayobj_cdict, file_dict, filename): - logger.debug('_filespec_additional_attachments filename=%s', filename) - md5sum_bytes = hashlib.md5(file_dict['filedata']).digest() + pdf_writer, name_arrayobj_cdict, file_dict, filename +): + logger.debug("_filespec_additional_attachments filename=%s", filename) + md5sum_bytes = hashlib.md5(file_dict["filedata"]).digest() md5sum_obj = ByteStringObject(md5sum_bytes) - params_dict = DictionaryObject({ - NameObject('/CheckSum'): md5sum_obj, - NameObject('/Size'): NumberObject(len(file_dict['filedata'])), - }) + params_dict = DictionaryObject( + { + NameObject("/CheckSum"): md5sum_obj, + NameObject("/Size"): NumberObject(len(file_dict["filedata"])), + } + ) # creation date and modification date are optional - if isinstance(file_dict.get('modification_datetime'), datetime): - mod_date_pdf = _get_pdf_timestamp(file_dict['modification_datetime']) - params_dict[NameObject('/ModDate')] = create_string_object(mod_date_pdf) - if isinstance(file_dict.get('creation_datetime'), datetime): - creation_date_pdf = _get_pdf_timestamp(file_dict['creation_datetime']) - params_dict[NameObject('/CreationDate')] = create_string_object( - creation_date_pdf) + if isinstance(file_dict.get("modification_datetime"), datetime): + mod_date_pdf = _get_pdf_timestamp(file_dict["modification_datetime"]) + params_dict[NameObject("/ModDate")] = create_string_object(mod_date_pdf) + if isinstance(file_dict.get("creation_datetime"), datetime): + creation_date_pdf = _get_pdf_timestamp(file_dict["creation_datetime"]) + params_dict[NameObject("/CreationDate")] = create_string_object( + creation_date_pdf + ) file_entry = DecodedStreamObject() - file_entry.set_data(file_dict['filedata']) + file_entry.set_data(file_dict["filedata"]) file_entry = file_entry.flate_encode() file_mimetype = mimetypes.guess_type(filename)[0] if not file_mimetype: - file_mimetype = 'application/octet-stream' - file_entry.update({ - NameObject("/Type"): NameObject("/EmbeddedFile"), - NameObject("/Params"): params_dict, - NameObject("/Subtype"): NameObject('/%s' % file_mimetype), - }) + file_mimetype = "application/octet-stream" + file_entry.update( + { + NameObject("/Type"): NameObject("/EmbeddedFile"), + NameObject("/Params"): params_dict, + NameObject("/Subtype"): NameObject(f"/{file_mimetype}"), + } + ) file_entry_obj = pdf_writer._add_object(file_entry) - ef_dict = DictionaryObject({ - NameObject("/F"): file_entry_obj, - }) + ef_dict = DictionaryObject( + { + NameObject("/F"): file_entry_obj, + } + ) fname_obj = create_string_object(filename) - afrelationship = file_dict.get('afrelationship') + afrelationship = file_dict.get("afrelationship") if afrelationship not in ATTACHMENTS_AFRelationship: - afrelationship = 'unspecified' - filespec_dict = DictionaryObject({ - NameObject("/AFRelationship"): NameObject("/%s" % afrelationship.capitalize()), - NameObject("/Desc"): create_string_object(file_dict.get('description', '')), - NameObject("/Type"): NameObject("/Filespec"), - NameObject("/F"): fname_obj, - NameObject("/EF"): ef_dict, - NameObject("/UF"): fname_obj, - }) + afrelationship = "unspecified" + filespec_dict = DictionaryObject( + { + NameObject("/AFRelationship"): NameObject( + f"/{afrelationship.capitalize()}" + ), + NameObject("/Desc"): create_string_object(file_dict.get("description", "")), + NameObject("/Type"): NameObject("/Filespec"), + NameObject("/F"): fname_obj, + NameObject("/EF"): ef_dict, + NameObject("/UF"): fname_obj, + } + ) filespec_obj = pdf_writer._add_object(filespec_dict) name_arrayobj_cdict[fname_obj] = filespec_obj def _facturx_update_metadata_add_attachment( - pdf_writer, xml_bytes, pdf_metadata, flavor, level, orderx_type=None, - lang=None, additional_attachments={}, afrelationship='data', - xmp_compression=True): - '''This method is inspired from the code of the add_attachment() - method of the pypdf lib''' + pdf_writer, + xml_bytes, + pdf_metadata, + flavor, + level, + orderx_type=None, + lang=None, + additional_attachments=None, + afrelationship="data", + xmp_compression=True, +): + """This method is inspired from the code of the add_attachment() + method of the pypdf lib""" + if additional_attachments is None: + additional_attachments = {} # The entry for the file # facturx_xml_str = facturx_xml_str.encode('utf-8') - if flavor == 'order-x' and orderx_type not in ORDERX_TYPES: + if flavor == "order-x" and orderx_type not in ORDERX_TYPES: raise ValueError( - 'Wrong value for orderx_type (%s), must be in %s' - % (orderx_type, ORDERX_TYPES)) + f"Wrong value for orderx_type ({orderx_type}), must be in {ORDERX_TYPES}" + ) if afrelationship not in XML_AFRelationship: raise ValueError( - "Wrong value for afrelationship (%s). Possible values: %s." - % (afrelationship, XML_AFRelationship)) + f"Wrong value for afrelationship ({afrelationship}). " + f"Possible values: {XML_AFRelationship}." + ) md5sum_bytes = hashlib.md5(xml_bytes).digest() md5sum_obj = ByteStringObject(md5sum_bytes) - params_dict = DictionaryObject({ - NameObject('/CheckSum'): md5sum_obj, - NameObject('/ModDate'): create_string_object(_get_pdf_timestamp()), - NameObject('/Size'): NumberObject(len(xml_bytes)), - }) + params_dict = DictionaryObject( + { + NameObject("/CheckSum"): md5sum_obj, + NameObject("/ModDate"): create_string_object(_get_pdf_timestamp()), + NameObject("/Size"): NumberObject(len(xml_bytes)), + } + ) file_entry = DecodedStreamObject() file_entry.set_data(xml_bytes) # here we integrate the file itself file_entry = file_entry.flate_encode() - file_entry.update({ - NameObject("/Type"): NameObject("/EmbeddedFile"), - NameObject("/Params"): params_dict, - NameObject("/Subtype"): NameObject("/text/xml"), - }) + file_entry.update( + { + NameObject("/Type"): NameObject("/EmbeddedFile"), + NameObject("/Params"): params_dict, + NameObject("/Subtype"): NameObject("/text/xml"), + } + ) file_entry_obj = pdf_writer._add_object(file_entry) # The Filespec entry - ef_dict = DictionaryObject({ - NameObject("/F"): file_entry_obj, - NameObject('/UF'): file_entry_obj, - }) + ef_dict = DictionaryObject( + { + NameObject("/F"): file_entry_obj, + NameObject("/UF"): file_entry_obj, + } + ) - if flavor == 'order-x': + if flavor == "order-x": xml_filename = ORDERX_FILENAME - desc = 'Order-X XML file' + desc = "Order-X XML file" else: xml_filename = FACTURX_FILENAME - desc = 'Factur-X XML file' + desc = "Factur-X XML file" fname_obj = create_string_object(xml_filename) - filespec_dict = DictionaryObject({ - NameObject("/AFRelationship"): NameObject("/%s" % afrelationship.capitalize()), - NameObject("/Desc"): create_string_object(desc), - NameObject("/Type"): NameObject("/Filespec"), - NameObject("/F"): fname_obj, - NameObject("/EF"): ef_dict, - NameObject("/UF"): fname_obj, - }) + filespec_dict = DictionaryObject( + { + NameObject("/AFRelationship"): NameObject( + f"/{afrelationship.capitalize()}" + ), + NameObject("/Desc"): create_string_object(desc), + NameObject("/Type"): NameObject("/Filespec"), + NameObject("/F"): fname_obj, + NameObject("/EF"): ef_dict, + NameObject("/UF"): fname_obj, + } + ) filespec_obj = pdf_writer._add_object(filespec_dict) name_arrayobj_cdict = {fname_obj: filespec_obj} for attach_filename, attach_dict in additional_attachments.items(): _filespec_additional_attachments( - pdf_writer, name_arrayobj_cdict, attach_dict, attach_filename) - logger.debug('name_arrayobj_cdict=%s', name_arrayobj_cdict) + pdf_writer, name_arrayobj_cdict, attach_dict, attach_filename + ) + logger.debug("name_arrayobj_cdict=%s", name_arrayobj_cdict) name_arrayobj_content_sort = list( - sorted(name_arrayobj_cdict.items(), key=lambda x: x[0])) - logger.debug('name_arrayobj_content_sort=%s', name_arrayobj_content_sort) + sorted(name_arrayobj_cdict.items(), key=lambda x: x[0]) + ) + logger.debug("name_arrayobj_content_sort=%s", name_arrayobj_content_sort) name_arrayobj_content_final = [] af_list = [] - for (fname_obj, filespec_obj) in name_arrayobj_content_sort: + for fname_obj, filespec_obj in name_arrayobj_content_sort: name_arrayobj_content_final += [fname_obj, filespec_obj] af_list.append(filespec_obj) - embedded_files_names_dict = DictionaryObject({ - NameObject("/Names"): ArrayObject(name_arrayobj_content_final), - }) + embedded_files_names_dict = DictionaryObject( + { + NameObject("/Names"): ArrayObject(name_arrayobj_content_final), + } + ) # Then create the entry for the root, as it needs a # reference to the Filespec - embedded_files_dict = DictionaryObject({ - NameObject("/EmbeddedFiles"): embedded_files_names_dict, - }) + embedded_files_dict = DictionaryObject( + { + NameObject("/EmbeddedFiles"): embedded_files_names_dict, + } + ) # Update the root af_value_obj = pdf_writer._add_object(ArrayObject(af_list)) update_root_dict = { @@ -770,124 +846,140 @@ def _facturx_update_metadata_add_attachment( NameObject("/Names"): embedded_files_dict, # show attachments when opening PDF NameObject("/PageMode"): NameObject("/UseAttachments"), - } + } metadata_xml_bytes = _prepare_pdf_metadata_xml( - flavor, level, orderx_type, pdf_metadata) + flavor, level, orderx_type, pdf_metadata + ) metadata_file_entry = DecodedStreamObject() - metadata_file_entry.update({ - NameObject('/Subtype'): NameObject('/XML'), - NameObject('/Type'): NameObject('/Metadata'), - }) + metadata_file_entry.update( + { + NameObject("/Subtype"): NameObject("/XML"), + NameObject("/Type"): NameObject("/Metadata"), + } + ) metadata_file_entry.set_data(metadata_xml_bytes) if xmp_compression: metadata_file_entry = metadata_file_entry.flate_encode() - existing_metadata_obj = pdf_writer._root_object.get('/Metadata') + existing_metadata_obj = pdf_writer._root_object.get("/Metadata") if existing_metadata_obj: - logger.debug('Found existing /Metadata entry in catalog: replacing it.') + logger.debug("Found existing /Metadata entry in catalog: replacing it.") pdf_writer._replace_object(existing_metadata_obj, metadata_file_entry) else: - logger.debug('No existing /Metadata entry in catalog: creating one.') + logger.debug("No existing /Metadata entry in catalog: creating one.") metadata_obj = pdf_writer._add_object(metadata_file_entry) update_root_dict[NameObject("/Metadata")] = metadata_obj pdf_writer._root_object.update(update_root_dict) if lang: - pdf_writer._root_object.update({ - NameObject("/Lang"): create_string_object(lang.replace('_', '-')), - }) + pdf_writer._root_object.update( + { + NameObject("/Lang"): create_string_object(lang.replace("_", "-")), + } + ) metadata_txt_dict = _prepare_pdf_metadata_txt(pdf_metadata) pdf_writer.add_metadata(metadata_txt_dict) - logger.info('%s file added to PDF document', xml_filename) + logger.info("%s file added to PDF document", xml_filename) def _extract_base_info(facturx_xml_etree, flavor): - if flavor not in ('factur-x', 'facturx', 'order-x', 'orderx', 'zugferd'): + if flavor not in ("factur-x", "facturx", "order-x", "orderx", "zugferd"): raise ValueError("Wrong value for flavor argument.") namespaces = get_xml_namespaces(flavor) date_xpath = facturx_xml_etree.xpath( - '//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString', - namespaces=namespaces) + "//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString", + namespaces=namespaces, + ) date = date_xpath[0].text - date_format = date_xpath[0].attrib and date_xpath[0].attrib.get('format') or '102' + date_format = date_xpath[0].attrib and date_xpath[0].attrib.get("format") or "102" format_map = { - '102': '%Y%m%d', - '203': '%Y%m%d%H%M', - } - date_dt = datetime.strptime(date, format_map.get(date_format, format_map['102'])) + "102": "%Y%m%d", + "203": "%Y%m%d%H%M", + } + date_dt = datetime.strptime(date, format_map.get(date_format, format_map["102"])) number_xpath = facturx_xml_etree.xpath( - '//rsm:ExchangedDocument/ram:ID', namespaces=namespaces) + "//rsm:ExchangedDocument/ram:ID", namespaces=namespaces + ) number = number_xpath[0].text seller_xpath = facturx_xml_etree.xpath( - '//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:Name', - namespaces=namespaces) + "//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:Name", + namespaces=namespaces, + ) seller = seller_xpath[0].text buyer_xpath = facturx_xml_etree.xpath( - '//ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:Name', - namespaces=namespaces) + "//ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:Name", + namespaces=namespaces, + ) buyer = buyer_xpath[0].text doc_type_xpath = facturx_xml_etree.xpath( - '//rsm:ExchangedDocument/ram:TypeCode', namespaces=namespaces) + "//rsm:ExchangedDocument/ram:TypeCode", namespaces=namespaces + ) doc_type = doc_type_xpath[0].text base_info = { - 'seller': seller, - 'buyer': buyer, - 'number': number, - 'date': date_dt, - 'doc_type': doc_type, - } - logger.debug('Extraction of base_info: %s', base_info) + "seller": seller, + "buyer": buyer, + "number": number, + "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): doc_type_map = { - '220': 'Order', - '230': 'Order Change', - '231': 'Order Response', - '380': 'Invoice', - '381': 'Refund', - } - doc_type_name = doc_type_map.get(base_info['doc_type'], 'Invoice') - date_str = datetime.strftime(base_info['date'], '%Y-%m-%d') - if base_info['doc_type'] == '231': - title = '%s: Order Response on Order %s from %s' % ( - base_info['seller'], base_info['number'], base_info['buyer']) - subject = 'Response of %s on %s to order %s from %s' % ( - base_info['seller'], date_str, base_info['number'], base_info['buyer']) - doc_x = 'Order-X' - author = base_info['seller'] - elif base_info['doc_type'] in ('220', '230'): - title = '%s: %s %s' % ( - base_info['buyer'], doc_type_name, base_info['number']) - subject = '%s %s issued by %s on %s' % ( - doc_type_name, base_info['number'], base_info['buyer'], date_str) - doc_x = 'Order-X' - author = base_info['buyer'] + "220": "Order", + "230": "Order Change", + "231": "Order Response", + "380": "Invoice", + "381": "Refund", + } + doc_type_name = doc_type_map.get(base_info["doc_type"], "Invoice") + date_str = datetime.strftime(base_info["date"], "%Y-%m-%d") + if base_info["doc_type"] == "231": + title = ( + f"{base_info['seller']}: Order Response on Order {base_info['number']} " + f"from {base_info['buyer']}" + ) + subject = ( + f"Response of {base_info['seller']} on {date_str} to order " + f"{base_info['number']} from {base_info['buyer']}" + ) + doc_x = "Order-X" + author = base_info["seller"] + elif base_info["doc_type"] in ("220", "230"): + title = f"{base_info['buyer']}: {doc_type_name} {base_info['number']}" + subject = ( + f"{doc_type_name} {base_info['number']} issued by {base_info['buyer']} " + f"on {date_str}" + ) + doc_x = "Order-X" + author = base_info["buyer"] else: - title = '%s: %s %s' % ( - base_info['seller'], doc_type_name, base_info['number']) - subject = '%s %s dated %s issued by %s' % ( - doc_type_name, base_info['number'], date_str, base_info['seller']) - doc_x = 'Factur-X' - author = base_info['seller'] + title = f"{base_info['seller']}: {doc_type_name} {base_info['number']}" + subject = ( + f"{doc_type_name} {base_info['number']} dated {date_str} issued by " + f"{base_info['seller']}" + ) + doc_x = "Factur-X" + author = base_info["seller"] pdf_metadata = { - 'author': author, - 'keywords': '%s, %s' % (doc_type_name, doc_x), - 'title': title, - 'subject': subject, - } - logger.debug('Converted base_info to pdf_metadata: %s', pdf_metadata) + "author": author, + "keywords": f"{doc_type_name}, {doc_x}", + "title": title, + "subject": subject, + } + logger.debug("Converted base_info to pdf_metadata: %s", pdf_metadata) return pdf_metadata def get_xml_namespaces(flavor): - if flavor not in ('factur-x', 'facturx', 'order-x', 'orderx', 'zugferd'): + if flavor not in ("factur-x", "facturx", "order-x", "orderx", "zugferd"): raise ValueError("Wrong value for flavor argument.") - if flavor == 'facturx': - flavor = 'factur-x' - elif flavor == 'orderx': - flavor = 'order-x' + if flavor == "facturx": + flavor = "factur-x" + elif flavor == "orderx": + flavor = "order-x" return XML_NAMESPACES[flavor] @@ -895,25 +987,36 @@ def get_facturx_level(facturx_xml_etree): return get_level(facturx_xml_etree) -def get_level(xml_etree, flavor='autodetect'): - if not isinstance(xml_etree, type(etree.Element('pouet'))): - raise ValueError('xml_etree must be an etree.Element() object') - if flavor not in ('autodetect', 'factur-x', 'facturx', 'order-x', 'orderx', 'zugferd'): - raise ValueError('Wrong value for flavor argument.') - if flavor == 'autodetect': +def get_level(xml_etree, flavor="autodetect"): + if not isinstance(xml_etree, type(etree.Element("pouet"))): + raise ValueError("xml_etree must be an etree.Element() object") + if flavor not in ( + "autodetect", + "factur-x", + "facturx", + "order-x", + "orderx", + "zugferd", + ): + raise ValueError("Wrong value for flavor argument.") + if flavor == "autodetect": flavor = get_flavor(xml_etree) namespaces = get_xml_namespaces(flavor) # Factur-X and Order-X doc_id_xpath = xml_etree.xpath( "//rsm:ExchangedDocumentContext" "/ram:GuidelineSpecifiedDocumentContextParameter" - "/ram:ID", namespaces=namespaces) + "/ram:ID", + namespaces=namespaces, + ) if not doc_id_xpath: # ZUGFeRD 1.0 doc_id_xpath = xml_etree.xpath( "//rsm:SpecifiedExchangedDocumentContext" "/ram:GuidelineSpecifiedDocumentContextParameter" - "/ram:ID", namespaces=namespaces) + "/ram:ID", + namespaces=namespaces, + ) if not doc_id_xpath: raise ValueError( "This XML is not a Factur-X nor Order-X XML because it misses the XML tag " @@ -921,7 +1024,8 @@ def get_level(xml_etree, flavor='autodetect'): "GuidelineSpecifiedDocumentContextParameter/ID. It is not a ZUGFeRD 1.0 " "XML either because it misses the XML tag " "SpecifiedExchangedDocumentContext/" - "GuidelineSpecifiedDocumentContextParameter/ID.") + "GuidelineSpecifiedDocumentContextParameter/ID." + ) doc_id = doc_id_xpath[0].text # Content of the ID field per level for Factur-X: # minimum: urn:factur-x.eu:1p0:minimum @@ -938,60 +1042,72 @@ def get_level(xml_etree, flavor='autodetect'): # ZUGFeRD 1.0 levels are the same as orderx possible_values = dict(FACTURX_LEVEL2xsd) possible_values.update(ORDERX_LEVEL2xsd) - level = doc_id.split(':')[-1] + level = doc_id.split(":")[-1] if level == "extended-ctc-fr": level = "extended" if level not in possible_values: # Ignore what is after the first "#" - doc_id_cut = doc_id.split('#')[0] + doc_id_cut = doc_id.split("#")[0] if len(doc_id_cut) > 1: - level = doc_id_cut.split(':')[-2] + level = doc_id_cut.split(":")[-2] if level not in possible_values: - raise ValueError( - "Invalid Factur-X/Order-X URN: '%s'" % doc_id) - logger.info('Level is %s (autodetected)', level) + raise ValueError(f"Invalid Factur-X/Order-X URN: '{doc_id}'") + logger.info("Level is %s (autodetected)", level) return level def get_flavor(xml_etree): - if not isinstance(xml_etree, type(etree.Element('pouet'))): - raise ValueError('xml_etree must be an etree.Element() object') - logger.debug('First XML tag: %s', xml_etree.tag) - if xml_etree.tag.endswith('CrossIndustryInvoice'): - flavor = 'factur-x' - elif xml_etree.tag.endswith('CrossIndustryDocument'): - flavor = 'zugferd' - elif xml_etree.tag.endswith('SCRDMCCBDACIOMessageStructure'): - flavor = 'order-x' + if not isinstance(xml_etree, type(etree.Element("pouet"))): + raise ValueError("xml_etree must be an etree.Element() object") + logger.debug("First XML tag: %s", xml_etree.tag) + if xml_etree.tag.endswith("CrossIndustryInvoice"): + flavor = "factur-x" + elif xml_etree.tag.endswith("CrossIndustryDocument"): + flavor = "zugferd" + elif xml_etree.tag.endswith("SCRDMCCBDACIOMessageStructure"): + flavor = "order-x" else: raise Exception( "Could not detect if the document is a Factur-X, ZUGFeRD 1.0 " - "or Order-X document.") - logger.info('Flavor is %s (autodetected)', flavor) + "or Order-X document." + ) + logger.info("Flavor is %s (autodetected)", flavor) return flavor def get_orderx_type(xml_etree): - if not isinstance(xml_etree, type(etree.Element('pouet'))): - raise ValueError('xml_etree must be an etree.Element() object') - type_code_xpath = \ + if not isinstance(xml_etree, type(etree.Element("pouet"))): + raise ValueError("xml_etree must be an etree.Element() object") + type_code_xpath = ( "/rsm:SCRDMCCBDACIOMessageStructure/rsm:ExchangedDocument/ram:TypeCode" - xpath_res = xml_etree.xpath(type_code_xpath, namespaces=XML_NAMESPACES['order-x']) + ) + xpath_res = xml_etree.xpath(type_code_xpath, namespaces=XML_NAMESPACES["order-x"]) code = xpath_res and xpath_res[0].text and xpath_res[0].text.strip() or None if code not in ORDERX_code2type: raise Exception( - "The TypeCode extracted from the XML is %s. " - "This is not a valid Order-X TypeCode." % code) + f"The TypeCode extracted from the XML is {code}. " + f"This is not a valid Order-X TypeCode." + ) logger.info( - 'Order-X type is %s code %s (autodetected)', ORDERX_code2type[code], code) + "Order-X type is %s code %s (autodetected)", ORDERX_code2type[code], code + ) return ORDERX_code2type[code] def generate_from_binary( - pdf_file, xml, flavor='autodetect', level='autodetect', - orderx_type='autodetect', - check_xsd=True, check_schematron=True, pdf_metadata=None, lang=None, - attachments=None, afrelationship='data', xmp_compression=True): + pdf_file, + xml, + flavor="autodetect", + level="autodetect", + orderx_type="autodetect", + check_xsd=True, + check_schematron=True, + pdf_metadata=None, + lang=None, + attachments=None, + afrelationship="data", + xmp_compression=True, +): """ Generate a Factur-X or Order-X PDF from a regular PDF and a factur-X or Order-X XML file. The method uses a binary as input (the regular PDF) @@ -1068,16 +1184,24 @@ def generate_from_binary( """ if not isinstance(pdf_file, bytes): - raise ValueError('pdf_invoice argument must be a string') + raise ValueError("pdf_invoice argument must be a string") result_pdf = False - with NamedTemporaryFile(prefix='facturx-', suffix='.pdf') as f: + with NamedTemporaryFile(prefix="facturx-", suffix=".pdf") as f: f.write(pdf_file) generate_from_file( - f, xml, flavor=flavor, level=level, orderx_type=orderx_type, - check_xsd=check_xsd, check_schematron=check_schematron, - pdf_metadata=pdf_metadata, lang=lang, - attachments=attachments, afrelationship=afrelationship, - xmp_compression=xmp_compression) + f, + xml, + flavor=flavor, + level=level, + orderx_type=orderx_type, + check_xsd=check_xsd, + check_schematron=check_schematron, + pdf_metadata=pdf_metadata, + lang=lang, + attachments=attachments, + afrelationship=afrelationship, + xmp_compression=xmp_compression, + ) f.seek(0) result_pdf = f.read() f.close() @@ -1085,11 +1209,20 @@ def generate_from_binary( def generate_from_file( - pdf_file, xml, flavor='autodetect', level='autodetect', - orderx_type='autodetect', - check_xsd=True, check_schematron=True, pdf_metadata=None, lang=None, - output_pdf_file=None, - attachments=None, afrelationship='data', xmp_compression=True): + pdf_file, + xml, + flavor="autodetect", + level="autodetect", + orderx_type="autodetect", + check_xsd=True, + check_schematron=True, + pdf_metadata=None, + lang=None, + output_pdf_file=None, + attachments=None, + afrelationship="data", + xmp_compression=True, +): """ Generate a Factur-X or Order-X PDF file from a regular PDF and a Factur-X or Order-X XML file. The method uses a file as input (regular PDF file) @@ -1168,80 +1301,81 @@ def generate_from_file( :rtype: bool """ start_chrono = datetime.now() - logger.debug( - 'generate_from_file with factur-x lib %s', VERSION) - logger.debug('1st arg pdf_file type=%s', type(pdf_file)) - logger.debug('2nd arg xml type=%s', type(xml)) - logger.debug('optional arg flavor=%s', flavor) - logger.debug('optional arg level=%s', level) - logger.debug('optional arg orderx_type=%s', orderx_type) - logger.debug('optional arg check_xsd=%s', check_xsd) + logger.debug("generate_from_file with factur-x lib %s", VERSION) + logger.debug("1st arg pdf_file type=%s", type(pdf_file)) + logger.debug("2nd arg xml type=%s", type(xml)) + logger.debug("optional arg flavor=%s", flavor) + logger.debug("optional arg level=%s", level) + logger.debug("optional arg orderx_type=%s", orderx_type) + logger.debug("optional arg check_xsd=%s", check_xsd) logger.debug(f"optional arg check_schematron={check_schematron}") - logger.debug('optional arg pdf_metadata=%s', pdf_metadata) - logger.debug('optional arg lang=%s', lang) - logger.debug('optional arg output_pdf_file=%s', output_pdf_file) - logger.debug('optional arg attachments=%s', attachments) - logger.debug('optional arg afrelationship=%s', afrelationship) + logger.debug("optional arg pdf_metadata=%s", pdf_metadata) + logger.debug("optional arg lang=%s", lang) + logger.debug("optional arg output_pdf_file=%s", output_pdf_file) + logger.debug("optional arg attachments=%s", attachments) + logger.debug("optional arg afrelationship=%s", afrelationship) if not pdf_file: - raise ValueError('Missing pdf_file argument') + raise ValueError("Missing pdf_file argument") if not xml: - raise ValueError('Missing xml argument') + raise ValueError("Missing xml argument") if not isinstance(flavor, str): - raise ValueError('flavor argument is a %s, must be a string' % type(flavor)) + raise ValueError(f"flavor argument is a {type(flavor)}, must be a string") if not isinstance(level, str): - raise ValueError('level argument is a %s, must be a string' % type(level)) + raise ValueError(f"level argument is a {type(level)}, must be a string") if not isinstance(orderx_type, (str, type(None))): raise ValueError( - 'orderx_type argument is a %s, must be a string or None' - % type(orderx_type)) + f"orderx_type argument is a {type(orderx_type)}, must be a string or None" + ) if not isinstance(check_xsd, bool): - raise ValueError( - 'check_xsd argument is a %s, must be a boolean' % type(check_xsd)) + raise ValueError(f"check_xsd argument is a {type(check_xsd)}, must be boolean") if not isinstance(check_schematron, bool): raise ValueError( - "check_schematron argument is a {type(check_schematron)}, must be a boolean") + "check_schematron argument is a {type(check_schematron)}, must be a boolean" + ) if not isinstance(pdf_metadata, (dict, type(None))): raise ValueError( - 'pdf_metadata argument is a %s, must be a dict or None' - % type(pdf_metadata)) + f"pdf_metadata argument is a {type(pdf_metadata)}, must be a dict or None" + ) if not isinstance(lang, (type(None), str)): - raise ValueError( - 'lang argument is a %s, must be a string or None' % type(lang)) + raise ValueError(f"lang argument is a {type(lang)}, must be a string or None") if not isinstance(output_pdf_file, (type(None), str)): raise ValueError( - 'output_pdf_file argument is a %s, must be a string or None' - % type(output_pdf_file)) + f"output_pdf_file argument is a {type(output_pdf_file)}, " + f"must be a string or None" + ) if not isinstance(attachments, (dict, type(None))): raise ValueError( - 'attachments argument is a %s, must be a dict or None' % type(attachments)) + f"attachments argument is a {type(attachments)}, must be a dict or None" + ) if not isinstance(afrelationship, (str, type(None))): raise ValueError( - 'afrelationship argument is a %s, must be a string or None' - % type(afrelationship)) + f"afrelationship argument is a {type(afrelationship)}, " + f"must be a string or None" + ) # Tolerance on arguments - reformatting flavor = flavor.lower() flavor_fix_mapping = { - 'orderx': 'order-x', - 'facturx': 'factur-x', - } + "orderx": "order-x", + "facturx": "factur-x", + } flavor = flavor_fix_mapping.get(flavor, flavor) - level = level.lower().replace(' ', '') + level = level.lower().replace(" ", "") if orderx_type: - orderx_type = orderx_type.lower().replace('-', '_').replace(' ', '_') + orderx_type = orderx_type.lower().replace("-", "_").replace(" ", "_") if afrelationship: afrelationship = afrelationship.lower() else: - afrelationship = 'data' + afrelationship = "data" if afrelationship not in XML_AFRelationship: logger.warning( - "Wrong value for afrelationship (%s). Forcing it to 'data'.", - afrelationship) - afrelationship = 'data' + "Wrong value for afrelationship (%s). Forcing it to 'data'.", afrelationship + ) + afrelationship = "data" if isinstance(pdf_file, str): - file_type = 'path' + file_type = "path" else: - file_type = 'file' + file_type = "file" xml_root = None if isinstance(xml, bytes): xml_bytes = xml @@ -1251,12 +1385,12 @@ def generate_from_file( with open(xml, "rb") as xml_file: xml_bytes = xml_file.read() else: - xml_bytes = xml.encode('utf8') - elif isinstance(xml, type(etree.Element('pouet'))): + xml_bytes = xml.encode("utf8") + elif isinstance(xml, type(etree.Element("pouet"))): xml_root = xml xml_bytes = etree.tostring( - xml_root, pretty_print=True, encoding='UTF-8', - xml_declaration=True) + xml_root, pretty_print=True, encoding="UTF-8", xml_declaration=True + ) elif isinstance(xml, IOBase): xml.seek(0) xml_bytes = xml.read() @@ -1265,9 +1399,9 @@ def generate_from_file( # I don't think we expect the lib to close it else: raise TypeError( - "The second argument of the method generate_from_file must be " - "either a string, an etree.Element() object or a file " - "(it is a %s)." % type(xml)) + f"The second argument of the method generate_from_file must be either a " + f"string, an etree.Element() object or a file (it is a {type(xml)})." + ) if attachments is None: attachments = {} if attachments: @@ -1276,63 +1410,69 @@ def generate_from_file( for filename in list(attachments.keys()): if filename in ALL_FILENAMES: logger.warning( - 'You cannot provide as attachment a file named %s. ' - 'This file will NOT be attached.', filename) + "You cannot provide as attachment a file named %s. " + "This file will NOT be attached.", + filename, + ) attachments.pop(filename) for fadict in attachments.values(): - if fadict.get('filepath') and not fadict.get('filedata'): - with open(fadict['filepath'], 'rb') as fa: + if fadict.get("filepath") and not fadict.get("filedata"): + with open(fadict["filepath"], "rb") as fa: fa.seek(0) - fadict['filedata'] = fa.read() + fadict["filedata"] = fa.read() fa.close() # As explained here # https://stackoverflow.com/questions/237079/how-to-get-file-creation-modification-date-times-in-python # creation date is not easy to get. # So we only implement getting the modification date - if not fadict.get('modification_datetime'): - mod_timestamp = os.path.getmtime(fadict['filepath']) - fadict['modification_datetime'] = datetime.fromtimestamp( - mod_timestamp) - if fadict.get('afrelationship'): - fadict['afrelationship'] = fadict['afrelationship'].lower() - if fadict.get('afrelationship') not in ATTACHMENTS_AFRelationship: + if not fadict.get("modification_datetime"): + mod_timestamp = os.path.getmtime(fadict["filepath"]) + fadict["modification_datetime"] = datetime.fromtimestamp( + mod_timestamp + ) + if fadict.get("afrelationship"): + fadict["afrelationship"] = fadict["afrelationship"].lower() + if fadict.get("afrelationship") not in ATTACHMENTS_AFRelationship: # set default value - fadict['afrelationship'] = 'unspecified' - if flavor not in ('factur-x', 'order-x'): + fadict["afrelationship"] = "unspecified" + if flavor not in ("factur-x", "order-x"): if xml_root is None: xml_root = etree.fromstring(xml_bytes) - logger.debug('Flavor will be autodetected') + logger.debug("Flavor will be autodetected") flavor = get_flavor(xml_root) - if flavor == 'zugferd': + if flavor == "zugferd": raise ValueError( "XML is ZUGFeRD 1.x. Generating ZUGFeRD 1.x PDF is not supported. " - "You should update the XML to ZUGFeRD 2.x.") - if ( - (flavor == 'factur-x' and level not in FACTURX_LEVEL2xsd) or - (flavor == 'order-x' and level not in ORDERX_LEVEL2xsd)): + "You should update the XML to ZUGFeRD 2.x." + ) + if (flavor == "factur-x" and level not in FACTURX_LEVEL2xsd) or ( + flavor == "order-x" and level not in ORDERX_LEVEL2xsd + ): if xml_root is None: xml_root = etree.fromstring(xml_bytes) - logger.debug('level will be autodetected') + logger.debug("level will be autodetected") level = get_level(xml_root, flavor) if ( - flavor == 'factur-x' and - level in ('minimum', 'basicwl') and - afrelationship in ('source', 'alternative')): + flavor == "factur-x" + and level in ("minimum", "basicwl") + and afrelationship in ("source", "alternative") + ): logger.warning( "afrelationship switched from '%s' to 'data' because it must be 'data' " - "for Factur-X profile '%s'.", afrelationship, level) - afrelationship = 'data' - if flavor == 'order-x' and orderx_type not in ORDERX_TYPES: + "for Factur-X profile '%s'.", + afrelationship, + level, + ) + afrelationship = "data" + if flavor == "order-x" and orderx_type not in ORDERX_TYPES: if xml_root is None: xml_root = etree.fromstring(xml_bytes) orderx_type = get_orderx_type(xml_root) if check_xsd: - xml_check_xsd( - xml_bytes, flavor=flavor, level=level) - if flavor in ('factur-x', 'order-x') and check_schematron: - xml_check_schematron( - xml_bytes, flavor=flavor, level=level) + xml_check_xsd(xml_bytes, flavor=flavor, level=level) + if flavor in ("factur-x", "order-x") and check_schematron: + xml_check_schematron(xml_bytes, flavor=flavor, level=level) if pdf_metadata is None: if xml_root is None: xml_root = etree.fromstring(xml_bytes) @@ -1342,31 +1482,39 @@ def generate_from_file( # clean-up pdf_metadata dict for key, value in pdf_metadata.items(): if not isinstance(value, str): - pdf_metadata[key] = '' + pdf_metadata[key] = "" pdf_reader = PdfReader(pdf_file) pdf_writer = PdfWriter() pdf_writer._header = b"%PDF-1.6" pdf_writer.clone_document_from_reader(pdf_reader) _facturx_update_metadata_add_attachment( - pdf_writer, xml_bytes, pdf_metadata, flavor, level, - orderx_type=orderx_type, lang=lang, + pdf_writer, + xml_bytes, + pdf_metadata, + flavor, + level, + orderx_type=orderx_type, + lang=lang, additional_attachments=attachments, afrelationship=afrelationship, - xmp_compression=xmp_compression) + xmp_compression=xmp_compression, + ) if output_pdf_file: - with open(output_pdf_file, 'wb') as output_f: + with open(output_pdf_file, "wb") as output_f: pdf_writer.write(output_f) output_f.close() else: - if file_type == 'path': - with open(pdf_file, 'wb') as f: + if file_type == "path": + with open(pdf_file, "wb") as f: pdf_writer.write(f) f.close() - elif file_type == 'file': + elif file_type == "file": pdf_writer.write(pdf_file) end_chrono = datetime.now() logger.info( - '%s PDF generated in %s sec', - flavor, (end_chrono - start_chrono).total_seconds()) + "%s PDF generated in %s sec", + flavor, + (end_chrono - start_chrono).total_seconds(), + ) return True diff --git a/facturx/scripts/__init__.py b/src/facturx/scripts/__init__.py similarity index 100% rename from facturx/scripts/__init__.py rename to src/facturx/scripts/__init__.py diff --git a/facturx/scripts/pdfextractxml.py b/src/facturx/scripts/pdfextractxml.py similarity index 53% rename from facturx/scripts/pdfextractxml.py rename to src/facturx/scripts/pdfextractxml.py index 4d6832d..cd731c4 100755 --- a/facturx/scripts/pdfextractxml.py +++ b/src/facturx/scripts/pdfextractxml.py @@ -2,11 +2,13 @@ # Copyright 2017-2023 Alexis de Lattre import argparse +import logging import sys -from facturx import get_xml_from_pdf, __version__ as fxversion +from os.path import isdir, isfile + +from facturx import __version__ as fxversion +from facturx import get_xml_from_pdf from facturx.facturx import logger -import logging -from os.path import isfile, isdir __author__ = "Alexis de Lattre " __date__ = "March 2026" @@ -14,53 +16,59 @@ def pdfextractxml(args): - logger.info('pdfextractxml version %s using factur-x lib version %s', __version__, fxversion) + logger.info( + "pdfextractxml version %s using factur-x lib version %s", __version__, fxversion + ) if args.log_level: log_level = args.log_level.lower() log_map = { - 'debug': logging.DEBUG, - 'info': logging.INFO, - 'warn': logging.WARN, - 'error': logging.ERROR, + "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())) + "Wrong value for log level (%s). Possible values: %s", + log_level, + ", ".join(log_map.keys()), + ) sys.exit(1) pdf_filename = args.facturx_orderx_file out_xml_filename = args.xml_file_to_create if not isfile(pdf_filename): - logger.error('Argument %s is not a filename', 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) + "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, 'rb') + pdf_file = open(pdf_filename, "rb") check_xsd = not args.disable_xsd_check check_schematron = not args.disable_schematron_check # The important line of code is below ! try: (xml_filename, xml_string) = get_xml_from_pdf( - pdf_file, check_xsd=check_xsd, check_schematron=check_schematron) + pdf_file, check_xsd=check_xsd, check_schematron=check_schematron + ) except Exception as e: logger.error(e) sys.exit(1) if xml_filename and xml_string: if isfile(out_xml_filename): - logger.warning( - 'File %s already exists. Overwriting it!', out_xml_filename) - xml_file = open(out_xml_filename, 'wb') + logger.warning("File %s already exists. Overwriting it!", out_xml_filename) + xml_file = open(out_xml_filename, "wb") xml_file.write(xml_string) xml_file.close() - logger.info('File %s generated', out_xml_filename) + logger.info("File %s generated", out_xml_filename) else: - logger.warning('File %s has not been created', out_xml_filename) + logger.warning("File %s has not been created", out_xml_filename) sys.exit(1) @@ -68,35 +76,46 @@ def main(args=None): if args is None: args = sys.argv[1:] usage = "facturx-pdfextractxml " - epilog = "Author: %s - Version: %s" % (__author__, __version__) + epilog = f"Author: {__author__} - Version: {__version__}" description = "This extracts the XML file from a Factur-X or Order-X PDF file." parser = argparse.ArgumentParser( - usage=usage, epilog=epilog, description=description) + usage=usage, epilog=epilog, description=description + ) parser.add_argument( - '-l', '--log-level', dest="log_level", default='info', + "-l", + "--log-level", + dest="log_level", + default="info", help="Set log level. Possible values: debug, info, warn, error. " - "Default value: info.") + "Default value: info.", + ) parser.add_argument( - '-d', '--disable-xsd-check', dest='disable_xsd_check', - action='store_true', + "-d", + "--disable-xsd-check", + dest="disable_xsd_check", + action="store_true", help="De-activate XML Schema Definition check on Factur-X/Order-X XML file " - "(the check is enabled by default)") + "(the check is enabled by default)", + ) parser.add_argument( - '-ds', '--disable-schematron-check', dest='disable_schematron_check', - action='store_true', + "-ds", + "--disable-schematron-check", + dest="disable_schematron_check", + action="store_true", help="De-activate Schematron check on Factur-X/Order-X XML file " - "(the check is enabled by default)") - parser.add_argument( - "facturx_orderx_file", help="PDF Factur-X or Order-X file") + "(the check is enabled by default)", + ) + parser.add_argument("facturx_orderx_file", help="PDF Factur-X or Order-X file") parser.add_argument( "xml_file_to_create", - help="Filename of the XML file that will be extracted from the PDF") + help="Filename of the XML file that will be extracted from the PDF", + ) args = parser.parse_args() pdfextractxml(args) def run(): - if __name__ == '__main__': + if __name__ == "__main__": main() diff --git a/src/facturx/scripts/pdfgen.py b/src/facturx/scripts/pdfgen.py new file mode 100755 index 0000000..2f849fd --- /dev/null +++ b/src/facturx/scripts/pdfgen.py @@ -0,0 +1,268 @@ +#! /usr/bin/env python +# Copyright 2017-2023 Alexis de Lattre + +import argparse +import logging +import sys +from os.path import basename, isdir, isfile + +from facturx import __version__ as fxversion +from facturx import generate_from_file +from facturx.facturx import logger + +__author__ = "Alexis de Lattre " +__date__ = "October 2025" +__version__ = "0.9" + + +def pdfgen(args): + logger.info( + "pdfgen version %s using factur-x lib version %s", __version__, fxversion + ) + if args.log_level: + log_level = args.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) + + pdf_filename = args.regular_pdf_file + output_pdf_filename = args.facturx_orderx_pdf_file + additional_attachment_filenames = args.optional_attachments + for filename in [pdf_filename, args.xml_file] + additional_attachment_filenames: + if not isfile(filename): + logger.error("Argument %s is not a filename", filename) + sys.exit(1) + if isdir(output_pdf_filename): + logger.error( + "3rd argument %s is a directory name (should be a the " + "Factur-X or Order-X PDF filename)", + output_pdf_filename, + ) + sys.exit(1) + check_xsd = not args.disable_xsd_check + check_schematron = not args.disable_schematron_check + pdf_metadata = None + if args.meta_author or args.meta_keywords or args.meta_title or args.meta_subject: + pdf_metadata = { + "author": args.meta_author, + "keywords": args.meta_keywords, + "title": args.meta_title, + "subject": args.meta_subject, + } + if isfile(output_pdf_filename): + if args.overwrite: + logger.warning( + "File %s already exists. Overwriting it.", output_pdf_filename + ) + else: + logger.error("File %s already exists. Exit.", output_pdf_filename) + sys.exit(1) + attachments = {} + for additional_attachment_filename in additional_attachment_filenames: + attachments[basename(additional_attachment_filename)] = { + "filepath": additional_attachment_filename + } + lang = args.lang or None + xmp_compression = not args.disable_xmp_compression + try: + # The important line of code is below ! + generate_from_file( + pdf_filename, + args.xml_file, + check_xsd=check_xsd, + check_schematron=check_schematron, + flavor=args.flavor, + level=args.level, + orderx_type=args.orderx_type, + pdf_metadata=pdf_metadata, + lang=lang, + output_pdf_file=output_pdf_filename, + attachments=attachments, + afrelationship=args.afrelationship, + xmp_compression=xmp_compression, + ) + except Exception: + # no need to re-print the error log, it is already present in the logs + logger.error("factur-x lib call failed, exiting.") + sys.exit(1) + + +def main(args=None): + if args is None: + args = sys.argv[1:] + usage = ( + "facturx-pdfgen " + " " + ) + epilog = f"Author: {__author__} - Version: {__version__}" + description = ( + "This script generate a Factur-X or Order-X PDF from a " + "regular PDF/A document and a Factur-X or Order-X XML file. " + "It can also include additional embedded files in the PDF. " + "To generate a valid PDF/A-3 document as requested by the " + "Factur-X/Order-X standards, you need to give a valid PDF/A " + "document as input." + "\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." + ) + parser = argparse.ArgumentParser( + usage=usage, epilog=epilog, description=description + ) + parser.add_argument( + "-l", + "--log-level", + dest="log_level", + default="info", + help="Set log level. Possible values: debug, info, warn, error. " + "Default value: info.", + ) + parser.add_argument( + "-d", + "--disable-xsd-check", + dest="disable_xsd_check", + action="store_true", + help="De-activate XML Schema Definition check on XML file " + "(the check is enabled by default)", + ) + parser.add_argument( + "-ds", + "--disable-schematron-check", + dest="disable_schematron_check", + action="store_true", + help="De-activate Schematron check on XML file " + "(the check is enabled by default)", + ) + parser.add_argument( + "-f", + "--flavor", + dest="flavor", + default="autodetect", + help="Specify if you want to generate a Factur-X or Order-X PDF file. " + "Default: autodetect. If you specify a particular flavor instead of " + "using autodetection from the XML, you will win a very small amount of time " + "(less than 1 millisecond). " + "Possible values: order-x, factur-x or autodetect.", + ) + parser.add_argument( + "-n", + "--level", + "--facturx-level", + dest="level", + default="autodetect", + help="Specify the Factur-X or Order-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 for Factur-X: minimum, basicwl, basic, en16931, extended." + "Possible values for Order-X: basic, comfort, extended.", + ) + parser.add_argument( + "-p", + "--orderx-type", + dest="orderx_type", + default="autodetect", + help="When you generate an Order-X document, specify the order type. " + "Default: autodetect. If you specify a particular order type instead of " + "using autodetection, you will win a very small amount of time " + "(less than 1 millisecond). " + "Possible values: order, order_change, order_response.", + ) + parser.add_argument( + "-g", + "--lang", + dest="lang", + help="Set the language identifier as RFC 3066 to specify the " + "natural language of the PDF document. Example: en-US.", + ) + parser.add_argument( + "-r", + "--afrelationship", + dest="afrelationship", + default="data", + help="Set the AFRelationship PDF property of the Factur-X/Order-X XML file. " + "Possible values: data, source, alternative. " + "Default value: data.", + ) + parser.add_argument( + "-a", + "--meta-author", + dest="meta_author", + help="Specify the author for PDF metadata. Default: use the vendor " + "name extracted from the XML file.", + ) + parser.add_argument( + "-k", + "--meta-keywords", + dest="meta_keywords", + help="Specify the keywords for PDF metadata. " + "Default for Factur-X: 'Invoice, Factur-X'." + "Default for Order-X: 'Order Change, Order-X' where 'Order Change' is " + "the order type.", + ) + parser.add_argument( + "-t", + "--meta-title", + dest="meta_title", + help="Specify the title of PDF metadata. " + "Default: generic English title with information extracted from " + "the XML file such as: 'Akretion: Invoice I1242'", + ) + parser.add_argument( + "-s", + "--meta-subject", + dest="meta_subject", + help="Specify the subject of PDF metadata. " + "Default: generic English subject with information extracted from the " + "XML file such as: " + "'Factur-X invoice I1242 dated 2017-08-17 issued by Akretion'", + ) + parser.add_argument( + "-nz", + "--disable-xmp-compression", + dest="disable_xmp_compression", + action="store_true", + help="Disable flate compression of the XMP metadata " + "(compression is enabled by default). You should disable compression of " + "the XMP metadata if you plan to later add a PAdES signature " + "on the generated PDF file.", + ) + parser.add_argument( + "-w", + "--overwrite", + dest="overwrite", + action="store_true", + help="Overwrite output PDF file if it already exists.", + ) + parser.add_argument("regular_pdf_file", help="Regular PDF invoice") + parser.add_argument("xml_file", help="Factur-X or Order-X XML file") + parser.add_argument( + "facturx_orderx_pdf_file", help="Generated Factur-X or Order-X PDF file" + ) + parser.add_argument( + "optional_attachments", + nargs="*", + help="Optional list of additionnal attachments", + ) + args = parser.parse_args() + pdfgen(args) + + +def run(): + if __name__ == "__main__": + main() + + +run() diff --git a/facturx/scripts/webservice.py b/src/facturx/scripts/webservice.py similarity index 52% rename from facturx/scripts/webservice.py rename to src/facturx/scripts/webservice.py index dbc84bb..dac7b4e 100755 --- a/facturx/scripts/webservice.py +++ b/src/facturx/scripts/webservice.py @@ -14,14 +14,17 @@ # -F 'xml=@/home/alexis/factur-x.xml' -o result_facturx.pdf # http://localhost:5000/generate_facturx -from flask import Flask, request, send_file -from tempfile import NamedTemporaryFile -from facturx import generate_from_file, __version__ as fxversion -from facturx.facturx import logger as fxlogger import argparse import logging import sys from logging.handlers import RotatingFileHandler +from tempfile import NamedTemporaryFile + +from flask import Flask, request, send_file + +from facturx import __version__ as fxversion +from facturx import generate_from_file +from facturx.facturx import logger as fxlogger MAX_ATTACHMENTS = 3 # TODO make it a cmd line option __author__ = "Alexis de Lattre " @@ -30,42 +33,47 @@ app = Flask(__name__) -@app.route('/generate_facturx', methods=['POST']) +@app.route("/generate_facturx", methods=["POST"]) def generate_facturx(): - app.logger.debug('request.files=%s', request.files) + app.logger.debug("request.files=%s", request.files) attachments = {} for i in range(MAX_ATTACHMENTS): - attach_key = 'attachment%d' % (i + 1) + attach_key = "attachment%d" % (i + 1) if request.files.get(attach_key): - with NamedTemporaryFile(prefix='fx-api-attach-') as attach_file: + with NamedTemporaryFile(prefix="fx-api-attach-") as attach_file: request.files[attach_key].save(attach_file.name) attach_file.seek(0) attachments[request.files[attach_key].filename] = { - 'filedata': attach_file.read(), - } + "filedata": attach_file.read(), + } attach_file.close() - with NamedTemporaryFile(prefix='fx-api-xml-', suffix='.xml') as xml_file: - request.files['xml'].save(xml_file.name) - app.logger.debug('xml_file.name=%s', xml_file.name) + with NamedTemporaryFile(prefix="fx-api-xml-", suffix=".xml") as xml_file: + request.files["xml"].save(xml_file.name) + app.logger.debug("xml_file.name=%s", xml_file.name) xml_file.seek(0) xml_byte = xml_file.read() xml_file.close() - res = '' - with NamedTemporaryFile(prefix='fx-api-pdf-', suffix='.pdf') as pdf_file: + res = "" + with NamedTemporaryFile(prefix="fx-api-pdf-", suffix=".pdf") as pdf_file: with NamedTemporaryFile( - prefix='fx-api-outpdf-', suffix='.pdf') as output_pdf_file: - request.files['pdf'].save(pdf_file.name) - app.logger.debug('pdf_file.name=%s', pdf_file.name) - app.logger.debug('output_pdf_file.name=%s', output_pdf_file.name) - app.logger.debug('attachments keys=%s', attachments.keys()) + prefix="fx-api-outpdf-", suffix=".pdf" + ) as output_pdf_file: + request.files["pdf"].save(pdf_file.name) + app.logger.debug("pdf_file.name=%s", pdf_file.name) + app.logger.debug("output_pdf_file.name=%s", output_pdf_file.name) + app.logger.debug("attachments keys=%s", attachments.keys()) generate_from_file( - pdf_file, xml_byte, output_pdf_file=output_pdf_file.name, - attachments=attachments) + pdf_file, + xml_byte, + output_pdf_file=output_pdf_file.name, + attachments=attachments, + ) output_pdf_file.seek(0) res = send_file(output_pdf_file.name, as_attachment=True) app.logger.info( - 'Factur-X or Order-X document successfully returned by webservice') + "Factur-X or Order-X document successfully returned by webservice" + ) output_pdf_file.close() pdf_file.close() return res @@ -75,46 +83,60 @@ def main(args=None): if args is None: args = sys.argv[1:] usage = "facturx_webservice.py [options]" - epilog = "Script written by Alexis de Lattre. "\ - "Published under the BSD licence." - description = "This is a Flask application that exposes a REST "\ - "webservice to generate a Factur-X invoice from a PDF file and an "\ + epilog = "Script written by Alexis de Lattre. " "Published under the BSD licence." + description = ( + "This is a Flask application that exposes a REST " + "webservice to generate a Factur-X invoice from a PDF file and an " "XML file." + ) parser = argparse.ArgumentParser( - usage=usage, epilog=epilog, description=description) + usage=usage, epilog=epilog, description=description + ) parser.add_argument( - '-s', '--host', dest='host', default='127.0.0.1', + "-s", + "--host", + dest="host", + default="127.0.0.1", help="The hostname to listen on. Defaults to '127.0.0.1': " - "the webservice will only accept connexions from localhost. Use " - "'0.0.0.0' to have the webservice available from a remote host (but " - "it is recommended to listen on localhost and use an HTTPS proxy to " - "listen to remote connexions).") + "the webservice will only accept connexions from localhost. Use " + "'0.0.0.0' to have the webservice available from a remote host (but " + "it is recommended to listen on localhost and use an HTTPS proxy to " + "listen to remote connexions).", + ) parser.add_argument( - '-p', '--port', dest='port', type=int, default=5000, + "-p", + "--port", + dest="port", + type=int, + default=5000, help="Port on which the webservice listens. You can select " - "any port between 1024 and 65535. Default port is 5000.") + "any port between 1024 and 65535. Default port is 5000.", + ) parser.add_argument( - '-d', '--debug', dest='debug', action='store_true', - help="Enable debug mode.") + "-d", "--debug", dest="debug", action="store_true", help="Enable debug mode." + ) parser.add_argument( - '-l', '--logfile', dest='logfile', - help="Logs to a file instead of stdout.") + "-l", "--logfile", dest="logfile", help="Logs to a file instead of stdout." + ) parser.add_argument( - '-n', '--loglevel', dest='loglevel', default='info', + "-n", + "--loglevel", + dest="loglevel", + default="info", help="Log level. Possible values: critical, error, warning, " - "info (default), debug.") + "info (default), debug.", + ) args = parser.parse_args() if args.logfile: - formatter = logging.Formatter( - "[%(asctime)s] %(levelname)s %(message)s") + formatter = logging.Formatter("[%(asctime)s] %(levelname)s %(message)s") handler = RotatingFileHandler(args.logfile) - if args.loglevel == 'debug': + if args.loglevel == "debug": level = logging.DEBUG - elif args.loglevel == 'critical': + elif args.loglevel == "critical": level = logging.CRITICAL - elif args.loglevel == 'warning': + elif args.loglevel == "warning": level = logging.WARNING - elif args.loglevel == 'error': + elif args.loglevel == "error": level = logging.ERROR else: level = logging.INFO @@ -123,13 +145,15 @@ def main(args=None): fxlogger.setLevel(level) fxlogger.addHandler(handler) app.logger.addHandler(handler) - app.logger.info('Start webservice to generate Factur-X invoices') - fxlogger.info('webservice version %s using factur-x lib version %s', __version__, fxversion) + app.logger.info("Start webservice to generate Factur-X invoices") + fxlogger.info( + "webservice version %s using factur-x lib version %s", __version__, fxversion + ) app.run(debug=args.debug, port=args.port, host=args.host) def run(): - if __name__ == '__main__': + if __name__ == "__main__": main() diff --git a/facturx/scripts/xmlcheck.py b/src/facturx/scripts/xmlcheck.py similarity index 52% rename from facturx/scripts/xmlcheck.py rename to src/facturx/scripts/xmlcheck.py index cd05c72..c673dc9 100755 --- a/facturx/scripts/xmlcheck.py +++ b/src/facturx/scripts/xmlcheck.py @@ -2,43 +2,48 @@ # Copyright 2017-2023 Alexis de Lattre import argparse -import sys -from facturx import xml_check_xsd, xml_check_schematron, __version__ as fxversion -from facturx.facturx import logger import logging +import sys from os.path import isfile +from facturx import __version__ as fxversion +from facturx import xml_check_schematron, xml_check_xsd +from facturx.facturx import logger + __author__ = "Alexis de Lattre " __date__ = "March 2026" __version__ = "0.5" def xmlcheck(args): - logger.info('xmlcheck version %s using factur-x lib version %s', __version__, fxversion) + logger.info( + "xmlcheck version %s using factur-x lib version %s", __version__, fxversion + ) if args.log_level: log_level = args.log_level.lower() log_map = { - 'debug': logging.DEBUG, - 'info': logging.INFO, - 'warn': logging.WARN, - 'error': logging.ERROR, + "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())) + "Wrong value for log level (%s). Possible values: %s", + log_level, + ", ".join(log_map.keys()), + ) sys.exit(1) if not isfile(args.xml_file): - logger.error('%s is not a filename', args.xml_file) + logger.error("%s is not a filename", args.xml_file) sys.exit(1) - with open(args.xml_file, 'rb') as xml_file: + with open(args.xml_file, "rb") as xml_file: xml_bytes = xml_file.read() try: - xml_check_xsd( - xml_bytes, flavor=args.flavor, level=args.level) + xml_check_xsd(xml_bytes, flavor=args.flavor, level=args.level) except Exception as e: logger.error(e) sys.exit(1) @@ -53,36 +58,51 @@ def main(args=None): if args is None: args = sys.argv[1:] usage = "facturx-xmlcheck " - epilog = "Author: %s - Version: %s" % (__author__, __version__) - description = "This script checks the Factur-X or Order-XML XML against the XML "\ - "Schema Definition." + epilog = f"Author: {__author__} - Version: {__version__}" + description = ( + "This script checks the Factur-X or Order-XML XML against the XML " + "Schema Definition." + ) parser = argparse.ArgumentParser( - usage=usage, epilog=epilog, description=description) + usage=usage, epilog=epilog, description=description + ) parser.add_argument( - '-l', '--log-level', dest='log_level', default='info', + "-l", + "--log-level", + dest="log_level", + default="info", help="Set log level. Possible values: debug, info, warn, error. " - "Default value: info.") + "Default value: info.", + ) parser.add_argument( - '-f', '--flavor', dest='flavor', default='autodetect', - help="Set XML flavor. Possible values: factur-x, zugferd, order-x or autodetect. " - "Default value: autodetect.") + "-f", + "--flavor", + dest="flavor", + default="autodetect", + help=( + "Set XML flavor. Possible values: factur-x, zugferd, order-x " + "or autodetect. Default value: autodetect." + ), + ) parser.add_argument( - '-n', '--facturx-level', dest='level', - default='autodetect', + "-n", + "--facturx-level", + dest="level", + default="autodetect", help="Specify the level of the Factur-X or Order-X 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 for Factur-X: minimum, basicwl, basic, en16931, extended. " - "Possible values for Order-X: basic, comfort, extended.") - parser.add_argument( - "xml_file", help="Factur-X or Order-X XML file to check") + "Possible values for Order-X: basic, comfort, extended.", + ) + parser.add_argument("xml_file", help="Factur-X or Order-X XML file to check") args = parser.parse_args() xmlcheck(args) def run(): - if __name__ == '__main__': + if __name__ == "__main__": main() diff --git a/facturx/xmp/Factur-X_extension_schema.xmp b/src/facturx/xmp/Factur-X_extension_schema.xmp similarity index 95% rename from facturx/xmp/Factur-X_extension_schema.xmp rename to src/facturx/xmp/Factur-X_extension_schema.xmp index 0639209..c59a257 100644 --- a/facturx/xmp/Factur-X_extension_schema.xmp +++ b/src/facturx/xmp/Factur-X_extension_schema.xmp @@ -23,14 +23,14 @@ Notes on the Factur-X schema namespace URI: --> - + BASIC factur-x.xml INVOICE 1.0 - + - \ No newline at end of file + diff --git a/facturx/xmp/ZUGFeRD_extension_schema.xmp b/src/facturx/xmp/ZUGFeRD_extension_schema.xmp similarity index 98% rename from facturx/xmp/ZUGFeRD_extension_schema.xmp rename to src/facturx/xmp/ZUGFeRD_extension_schema.xmp index d5e068c..3f39dc9 100644 --- a/facturx/xmp/ZUGFeRD_extension_schema.xmp +++ b/src/facturx/xmp/ZUGFeRD_extension_schema.xmp @@ -43,14 +43,14 @@ around bug #4433 in PDFlib 9.0.0 which has been fixed in PDFlib 9.0.1. --> - + BASIC ZUGFeRD-invoice.xml INVOICE 1.0 - + - \ No newline at end of file + diff --git a/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC-compiled.xsl b/src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC-compiled.xsl similarity index 99% rename from facturx/xsd/facturx-basic/Factur-X_1.08_BASIC-compiled.xsl rename to src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC-compiled.xsl index 8c44eb8..4eb2138 100644 --- a/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC-compiled.xsl +++ b/src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC-compiled.xsl @@ -10,7 +10,7 @@ xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" - version="2.0"> @@ -93,7 +93,7 @@ - diff --git a/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC.xsd b/src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC.xsd similarity index 100% rename from facturx/xsd/facturx-basic/Factur-X_1.08_BASIC.xsd rename to src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC.xsd diff --git a/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_codedb.xml b/src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_codedb.xml similarity index 100% rename from facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_codedb.xml rename to src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_codedb.xml diff --git a/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd b/src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd similarity index 100% rename from facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd rename to src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd diff --git a/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd b/src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd similarity index 100% rename from facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd rename to src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd diff --git a/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd b/src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd similarity index 100% rename from facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd rename to src/facturx/xsd/facturx-basic/Factur-X_1.08_BASIC_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd diff --git a/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL-compiled.xsl b/src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL-compiled.xsl similarity index 99% rename from facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL-compiled.xsl rename to src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL-compiled.xsl index ddddce4..cf84534 100644 --- a/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL-compiled.xsl +++ b/src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL-compiled.xsl @@ -10,7 +10,7 @@ xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" - version="2.0"> @@ -93,7 +93,7 @@ - diff --git a/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL.xsd b/src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL.xsd similarity index 100% rename from facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL.xsd rename to src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL.xsd diff --git a/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_codedb.xml b/src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_codedb.xml similarity index 100% rename from facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_codedb.xml rename to src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_codedb.xml diff --git a/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd b/src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd similarity index 100% rename from facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd rename to src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd diff --git a/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd b/src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd similarity index 100% rename from facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd rename to src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd diff --git a/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd b/src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd similarity index 100% rename from facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd rename to src/facturx/xsd/facturx-basicwl/Factur-X_1.08_BASICWL_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd diff --git a/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931-compiled.xsl b/src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931-compiled.xsl similarity index 99% rename from facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931-compiled.xsl rename to src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931-compiled.xsl index 9f35502..c5d9166 100644 --- a/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931-compiled.xsl +++ b/src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931-compiled.xsl @@ -10,7 +10,7 @@ xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" - version="2.0"> @@ -93,7 +93,7 @@ - diff --git a/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931.xsd b/src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931.xsd similarity index 100% rename from facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931.xsd rename to src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931.xsd diff --git a/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_codedb.xml b/src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_codedb.xml similarity index 100% rename from facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_codedb.xml rename to src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_codedb.xml diff --git a/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd b/src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd similarity index 100% rename from facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd rename to src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd diff --git a/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd b/src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd similarity index 100% rename from facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd rename to src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd diff --git a/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd b/src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd similarity index 100% rename from facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd rename to src/facturx/xsd/facturx-en16931/Factur-X_1.08_EN16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd diff --git a/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED-compiled.xsl b/src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED-compiled.xsl similarity index 99% rename from facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED-compiled.xsl rename to src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED-compiled.xsl index 05c5e8b..a5b8f53 100644 --- a/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED-compiled.xsl +++ b/src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED-compiled.xsl @@ -10,7 +10,7 @@ xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" - version="2.0"> @@ -93,7 +93,7 @@ - diff --git a/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED.xsd b/src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED.xsd similarity index 100% rename from facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED.xsd rename to src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED.xsd diff --git a/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_codedb.xml b/src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_codedb.xml similarity index 100% rename from facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_codedb.xml rename to src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_codedb.xml diff --git a/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd b/src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd similarity index 100% rename from facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd rename to src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd diff --git a/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd b/src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd similarity index 100% rename from facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd rename to src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd diff --git a/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd b/src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd similarity index 100% rename from facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd rename to src/facturx/xsd/facturx-extended/Factur-X_1.08_EXTENDED_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd diff --git a/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM-compiled.xsl b/src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM-compiled.xsl similarity index 99% rename from facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM-compiled.xsl rename to src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM-compiled.xsl index 32212a0..6b2810c 100644 --- a/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM-compiled.xsl +++ b/src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM-compiled.xsl @@ -10,7 +10,7 @@ xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" - version="2.0"> @@ -93,7 +93,7 @@ - diff --git a/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM.xsd b/src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM.xsd similarity index 100% rename from facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM.xsd rename to src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM.xsd diff --git a/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_codedb.xml b/src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_codedb.xml similarity index 100% rename from facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_codedb.xml rename to src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_codedb.xml diff --git a/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd b/src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd similarity index 100% rename from facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd rename to src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd diff --git a/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd b/src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd similarity index 100% rename from facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd rename to src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd diff --git a/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd b/src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd similarity index 100% rename from facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd rename to src/facturx/xsd/facturx-minimum/Factur-X_1.08_MINIMUM_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl similarity index 99% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl index 2948196..ff31136 100644 --- a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl +++ b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl @@ -164,7 +164,7 @@ xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" - version="1.0"> @@ -245,7 +245,7 @@ - diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd diff --git a/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd b/src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd similarity index 100% rename from facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd rename to src/facturx/xsd/orderx-basic/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl similarity index 99% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl index 1e04ae9..6fd865a 100644 --- a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl +++ b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl @@ -164,7 +164,7 @@ xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" - version="1.0"> @@ -245,7 +245,7 @@ - diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EDIFICAS-EU_AccountingAccountType_D11A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EDIFICAS-EU_AccountingAccountType_D11A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EDIFICAS-EU_AccountingAccountType_D11A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EDIFICAS-EU_AccountingAccountType_D11A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EN16931_AllowanceChargeReasonCode_D20A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EN16931_AllowanceChargeReasonCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EN16931_AllowanceChargeReasonCode_D20A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EN16931_AllowanceChargeReasonCode_D20A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ContactFunctionCode_D20A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ContactFunctionCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ContactFunctionCode_D20A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ContactFunctionCode_D20A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyTaxFeeTypeCode_D20A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyTaxFeeTypeCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyTaxFeeTypeCode_D20A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyTaxFeeTypeCode_D20A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyorTaxorFeeCategoryCode_D20A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyorTaxorFeeCategoryCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyorTaxorFeeCategoryCode_D20A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyorTaxorFeeCategoryCode_D20A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PackageTypeCode_2006.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PackageTypeCode_2006.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PackageTypeCode_2006.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PackageTypeCode_2006.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PaymentMeansCode_D20A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PaymentMeansCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PaymentMeansCode_D20A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PaymentMeansCode_D20A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ReferenceTypeCode_D20A.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ReferenceTypeCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ReferenceTypeCode_D20A.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ReferenceTypeCode_D20A.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd diff --git a/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd b/src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd similarity index 100% rename from facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd rename to src/facturx/xsd/orderx-comfort/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl similarity index 99% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl index 7201dfe..2c8a3b1 100644 --- a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl +++ b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B-compiled.xsl @@ -164,7 +164,7 @@ xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" - version="1.0"> @@ -245,7 +245,7 @@ - diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EDIFICAS-EU_AccountingAccountType_D11A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EDIFICAS-EU_AccountingAccountType_D11A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EDIFICAS-EU_AccountingAccountType_D11A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EDIFICAS-EU_AccountingAccountType_D11A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EN16931_AllowanceChargeReasonCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EN16931_AllowanceChargeReasonCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EN16931_AllowanceChargeReasonCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_EN16931_AllowanceChargeReasonCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_ISO_ISO3AlphaCurrencyCode_2012-08-31.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ActionCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ContactFunctionCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ContactFunctionCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ContactFunctionCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ContactFunctionCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsCode_2010.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DeliveryTermsFunctionCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentNameCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DocumentStatusCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyTaxFeeTypeCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyTaxFeeTypeCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyTaxFeeTypeCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyTaxFeeTypeCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyorTaxorFeeCategoryCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyorTaxorFeeCategoryCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyorTaxorFeeCategoryCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_DutyorTaxorFeeCategoryCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_EventTimeReferenceCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_EventTimeReferenceCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_EventTimeReferenceCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_EventTimeReferenceCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_MessageFunctionCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PackageTypeCode_2006.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PackageTypeCode_2006.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PackageTypeCode_2006.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PackageTypeCode_2006.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PaymentMeansCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PaymentMeansCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PaymentMeansCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_PaymentMeansCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ReferenceTypeCode_D20A.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ReferenceTypeCode_D20A.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ReferenceTypeCode_D20A.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_ReferenceTypeCode_D20A.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_TimePointFormatCode_D19B.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_TimePointFormatCode_D19B.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_TimePointFormatCode_D19B.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_codelist_standard_UNECE_TimePointFormatCode_D19B.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_QualifiedDataType_128.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_128.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_128.xsd diff --git a/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd b/src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd similarity index 100% rename from facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd rename to src/facturx/xsd/orderx-extended/SCRDMCCBDACIOMessageStructure_100pD20B_urn_un_unece_uncefact_identifierlist_standard_ISO_ISOTwo-letterCountryCode_SecondEdition2006.xsd diff --git a/facturx/xsd/zugferd/ZUGFeRD1p0.xsd b/src/facturx/xsd/zugferd/ZUGFeRD1p0.xsd similarity index 100% rename from facturx/xsd/zugferd/ZUGFeRD1p0.xsd rename to src/facturx/xsd/zugferd/ZUGFeRD1p0.xsd diff --git a/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_QualifiedDataType_12.xsd b/src/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_QualifiedDataType_12.xsd similarity index 100% rename from facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_QualifiedDataType_12.xsd rename to src/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_QualifiedDataType_12.xsd diff --git a/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_12.xsd b/src/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_12.xsd similarity index 100% rename from facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_12.xsd rename to src/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_12.xsd diff --git a/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_15.xsd b/src/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_15.xsd similarity index 100% rename from facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_15.xsd rename to src/facturx/xsd/zugferd/ZUGFeRD1p0_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_15.xsd diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/pdf/invoice_EN16931.pdf b/tests/fixtures/pdf/invoice_EN16931.pdf new file mode 100644 index 0000000..10a80bc Binary files /dev/null and b/tests/fixtures/pdf/invoice_EN16931.pdf differ diff --git a/tests/test_extraction.py b/tests/test_extraction.py new file mode 100644 index 0000000..7fd0f50 --- /dev/null +++ b/tests/test_extraction.py @@ -0,0 +1,17 @@ +# Copyright 2026 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import os +import unittest + +from facturx import get_xml_from_pdf + +current_dir = os.path.dirname(os.path.abspath(__file__)) + + +class TestAPI(unittest.TestCase): + def test_extraction(self): + path_file = os.path.join(current_dir, "fixtures/pdf/invoice_EN16931.pdf") + with open(path_file, "rb") as pdf_file: + get_xml_from_pdf(pdf_file, check_xsd=True, check_schematron=True)