add factur-x support (#20562)

The following configuration must be added to settings :

  VENDOR_NAME = 'Entr\'ouvert'
  VENDOR_SIRET = '44317013900036'
  VENDOR_TVAI = 'FR09491081899'
This commit is contained in:
Benjamin Dauvergne 2020-02-01 14:31:26 +01:00
parent 3985523fae
commit 4cbf6bdb13
12 changed files with 555 additions and 34 deletions

View File

@ -217,6 +217,7 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
'client',
'contrat',
'intitule',
'numero_engagement',
'notes',
'taux_tva',
'emission',

View File

@ -0,0 +1,91 @@
# barbacompta - invoicing for dummies
# Copyright (C) 2010-2012 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import os
import subprocess
import tempfile
from django.template.loader import render_to_string
import facturx
logger = logging.getLogger(__name__)
GS_INCLUDE_DIR = os.path.join(os.path.dirname(__file__), 'gs-includes')
DEFAULT_ICC_PROFILE = os.path.join(GS_INCLUDE_DIR, 'compatibleWithAdobeRGB1998.icc')
def to_pdfa(pdf_bytes: bytes, icc_profile: str = DEFAULT_ICC_PROFILE):
with tempfile.NamedTemporaryFile() as output_fd, tempfile.NamedTemporaryFile() as input_fd:
input_fd.write(pdf_bytes)
input_fd.flush()
args = [
'gs',
'-dPDFA',
'-dBATCH',
'-dNOPAUSE',
'-sICCProfile=' + icc_profile,
'-sProcessColorModel=DeviceCMYK',
'-sDEVICE=pdfwrite',
'-dPDFACompatibilityPolicy=2',
'-sOutputFile=' + output_fd.name,
# give right to GS of reading local PDF_def.ps and ICC profile
'-I' + GS_INCLUDE_DIR,
'PDFA_def.ps',
input_fd.name
]
logger.debug('converting to PDF/A, calling %s', args)
completed = subprocess.run(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
check=False)
logger.debug('ghostscript result %s', completed)
if completed.returncode != 0:
logger.error('ghostcript call failed %s', completed)
completed.check_returncode()
return output_fd.read()
def add_facturx_from_facture(facture):
pdf_bytes = facture.pdf()
facturx_context = {
'numero_de_facture': facture.code(),
'type_facture': '380',
'date_emission': facture.emission,
'chorus_service_code': facture.client.chorus_structure.service_code,
'vendeur': 'Entr\'ouvert',
'vendeur_siret': '44317013900036',
'vendeur_tvai': 'FR09491081899',
'client_siret': facture.client.chorus_structure.siret,
'client_name': facture.client.chorus_structure.name,
'numero_engagement': '',
'montant_ht': str(facture.montant),
'montant_ttc': str(facture.montant_ttc),
'montant_tva': str(facture.tva),
}
return add_facturx_from_bytes(pdf_bytes, facturx_context)
DEFAULT_FACTURX_TEMPLATE = 'factur-x.xml'
def add_facturx_from_bytes(pdf_bytes: bytes, facturx_context: dict, template: str = DEFAULT_FACTURX_TEMPLATE):
pdfa_bytes = to_pdfa(pdf_bytes)
facturx_xml = str(render_to_string(template, facturx_context)).encode('utf-8')
return facturx.generate_facturx_from_binary(pdfa_bytes, facturx_xml)

View File

@ -0,0 +1,79 @@
%!
% This is a sample prefix file for creating a PDF/A document.
% Users should modify entries marked with "Customize".
% This assumes an ICC profile resides in the file (srgb.icc),
% in the current directory unless the user modifies the corresponding line below.
% Define entries in the document Info dictionary :
[ /Title (Title) % Customise
/DOCINFO pdfmark
% Define an ICC profile :
% /ICCProfile (/usr/share/color/icc/compatibleWithAdobeRGB1998.icc) % Customise
% def
[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
%% This code attempts to set the /N (number of components) key for the ICC colour space.
%% To do this it checks the ColorConversionStrategy or the device ProcessColorModel if
%% ColorConversionStrategy is not set.
%% This is not 100% reliable. A better solution is for the user to edit this and replace
%% the code between the ---8<--- lines with a simple declaration like:
%% /N 3
%% where the value of N is the number of components from the profile defined in /ICCProfile above.
%%
[{icc_PDFA}
<<
%% ----------8<--------------8<-------------8<--------------8<----------
systemdict /ColorConversionStrategy known {
systemdict /ColorConversionStrategy get cvn dup /Gray eq {
pop /N 1 false
}{
dup /RGB eq {
pop /N 3 false
}{
/CMYK eq {
/N 4 false
}{
(ColorConversionStrategy not a device space, falling back to ProcessColorModel, output may not be valid PDF/A.)=
true
} ifelse
} ifelse
} ifelse
} {
(ColorConversionStrategy not set, falling back to ProcessColorModel, output may not be valid PDF/A.)=
true
} ifelse
{
currentpagedevice /ProcessColorModel get
dup /DeviceGray eq {
pop /N 1
}{
dup /DeviceRGB eq {
pop /N 3
}{
dup /DeviceCMYK eq {
pop /N 4
} {
(ProcessColorModel not a device space.)=
/ProcessColorModel cvx /rangecheck signalerror
} ifelse
} ifelse
} ifelse
} if
%% ----------8<--------------8<-------------8<--------------8<----------
>> /PUT pdfmark
[{icc_PDFA} ICCProfile (r) file /PUT pdfmark
% Define the output intent dictionary :
[/_objdef {OutputIntent_PDFA} /type /dict /OBJ pdfmark
[{OutputIntent_PDFA} <<
/Type /OutputIntent % Must be so (the standard requires).
/S /GTS_PDFA1 % Must be so (the standard requires).
/DestOutputProfile {icc_PDFA} % Must be so (see above).
/OutputConditionIdentifier (sRGB) % Customize
>> /PUT pdfmark
[{Catalog} <</OutputIntents [ {OutputIntent_PDFA} ]>> /PUT pdfmark

View File

@ -0,0 +1,34 @@
# Generated by Django 2.2.9 on 2020-02-01 14:59
from django.db import migrations, models
import eo_gestion.eo_facture.validators
class Migration(migrations.Migration):
dependencies = [
('eo_facture', '0007_auto_20191009_1335'),
]
operations = [
migrations.AddField(
model_name='client',
name='service_code',
field=models.CharField(blank=True, db_index=True, default='', max_length=128),
),
migrations.AddField(
model_name='client',
name='siret',
field=models.CharField(blank=True, db_index=True, default='', max_length=14, validators=[eo_gestion.eo_facture.validators.validate_siret]),
),
migrations.AddField(
model_name='contrat',
name='numero_marche',
field=models.CharField(blank=True, default='', max_length=128, verbose_name='Numéro du marché'),
),
migrations.AddField(
model_name='facture',
name='numero_engagement',
field=models.CharField(blank=True, default='', max_length=128, verbose_name="Numéro d'engagement"),
),
]

View File

@ -27,11 +27,14 @@ from django.core.validators import validate_email, RegexValidator
from django.conf import settings
from django.db.models.signals import post_save, post_delete
from django.db.models.query import QuerySet
from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _
from django.utils import six
from weasyprint import HTML
from ..eo_banque import models as banque_models
from . import fields
from . import fields, validators, facturx
from eo_gestion.utils import percentage_str
validate_telephone = RegexValidator(r"[. 0-9]*")
@ -57,10 +60,26 @@ class Client(models.Model):
verbose_name='TVA par défaut', max_digits=8, decimal_places=2, default=Decimal(DEFAULT_TVA)
)
picture = models.ImageField('Logo', upload_to='logos/', blank=True, null=True)
siret = models.CharField(
max_length=len('29202001300010'),
validators=[validators.validate_siret],
db_index=True,
blank=True,
default='',
)
service_code = models.CharField(
max_length=128,
db_index=True,
blank=True,
default='',
)
def __str__(self):
return self.nom
def clean(self):
self.service_code = self.service_code.strip()
class Meta:
ordering = ('nom',)
@ -85,6 +104,11 @@ class Contrat(models.Model):
percentage_per_year = fields.PercentagePerYearField(default=one_hundred_percent_this_year)
montant_sous_traite = models.DecimalField(max_digits=8, decimal_places=2, default=Decimal('0'))
image = models.ImageField('Image', upload_to='images/', blank=True, null=True)
numero_marche = models.CharField(
max_length=128,
verbose_name='Numéro du marché',
blank=True,
default='')
def montant_facture(self):
return sum([facture.montant for facture in self.factures.non_proforma()])
@ -112,6 +136,9 @@ class Contrat(models.Model):
"""
return self.client.nom
def clean(self):
self.numero_marche = self.numero_marche.strip()
def __str__(self):
return self.intitule
@ -200,6 +227,11 @@ class Facture(models.Model):
account_on_previous_period = models.BooleanField(
verbose_name='Mettre cette facture sur l\'exercice précédent', default=False
)
numero_engagement = models.CharField(
max_length=128,
verbose_name='Numéro d\'engagement',
blank=True,
default='')
objects = FactureQuerySet.as_manager()
@ -243,6 +275,7 @@ class Facture(models.Model):
except ValidationError:
raise ValidationError("Il y a un problème avec les lignes de cette facture")
self.update_paid(save=False)
self.numero_engagement = self.numero_engagement.strip()
def index(self):
if self.contrat:
@ -293,6 +326,35 @@ class Facture(models.Model):
return self.emission.year - 1
return self.emission.year
DEFAULT_FACTURE_TEMPLATE = 'facture.html'
def html(self, template_name=None, base_uri=None):
template = get_template(template_name or self.DEFAULT_FACTURE_TEMPLATE)
return template.render({'facture': self, 'base_uri': base_uri or ''})
def pdf(self, template_name=None, base_uri=None):
html = HTML(string=self.html(template_name=template_name, base_uri=base_uri))
pdf = html.write_pdf()
if not self.proforma:
facturx_ctx = {
'numero_de_facture': self.code(),
'type_facture': '380',
'date_emission': self.emission,
'vendeur': settings.VENDOR_NAME,
'vendeur_siret': settings.VENDOR_SIRET,
'vendeur_tvai': settings.VENDOR_TVAI,
'client_siret': self.client.siret,
'client_name': self.client.nom,
'chorus_service_code': self.client.service_code,
'numero_engagement': self.numero_engagement,
'numero_marche': (self.contrat and self.contrat.numero_marche) or '',
'montant_ht': str(self.montant),
'montant_ttc': str(self.montant_ttc),
'montant_tva': str(self.tva),
}
return facturx.add_facturx_from_bytes(pdf, facturx_ctx)
return pdf
class Meta:
ordering = ("-id",)

View File

@ -0,0 +1,89 @@
<rsm:CrossIndustryInvoice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
<rsm:ExchangedDocumentContext>
<ram:BusinessProcessSpecifiedDocumentContextParameter>
{% comment %}
CHORUSPRO : cette donnée permet de renseigner le cadre de facturation (facture de mandataire, de cotraitant, de sous-traitant, pièce de facturation d'un marché de travaux, etc.). Les codes à utiliser sont définis dans les spécifications CHORUSPRO : A1 (dépôt facture), A2 (dépôt facture déjà payée), ... Par défaut (en cas d'absence de ce champ), c'est le cas A1 qui s'applique.
{% endcomment %}
<ram:ID>{{ chorus_processus|default:"A1" }}</ram:ID>
</ram:BusinessProcessSpecifiedDocumentContextParameter>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:factur-x.eu:1p0:minimum</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>{{ numero_de_facture }}</ram:ID>
{% comment %}
Les types de document utilisés sont les suivants :
380 : Facture commerciale
381 : Avoir (note de crédit)
384 : Facture rectificative
389 : Facture d'autofacturation (créée par l'acheteur pour le compte du fournisseur)
261 : Avoir d'autofacturation (non accepté par CHORUSPRO)
386 : Facture d'acompte
751 : Informations de facture pour comptabilisation (non accepté par CHORUSPRO)
751 : Informations de facture pour comptabilisation (non accepté par CHORUSPRO)
{% endcomment %}
<ram:TypeCode>{{ type_facture }}</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">{{ date_emission|date:"Ymd" }}</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
{% comment %}
BuyerReference:
CHORUS PRO : pour le secteur public, il s'agit du "Service Executant". Il est obligatoire pour certains acheteurs. Il doit appartenir au référentiel Chorus Pro. Il est limité à 100 caractères
{% endcomment %}
{% if chorus_service_code %}
<ram:BuyerReference>{{ chorus_service_code }}</ram:BuyerReference>
{% endif %}
<ram:SellerTradeParty>
{% comment %}Entr'ouvert{% endcomment %}
<ram:Name>{{ vendeur }}</ram:Name>
<ram:SpecifiedLegalOrganization>
<ram:ID schemeID="0002">{{ vendeur_siret }}</ram:ID>
</ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress>
<ram:CountryID>FR</ram:CountryID>
</ram:PostalTradeAddress>
{% if vendeur_tvai %}
<ram:SpecifiedTaxRegistration>
{% comment %}Numéro de TVA intracommunautaie{% endcomment %}
<ram:ID schemeID="VA">{{ vendeur_tvai }}</ram:ID>
</ram:SpecifiedTaxRegistration>
{% endif %}
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
{% comment %}SIRET de l'acheteur{% endcomment %}
<ram:Name>{{ client_name }}</ram:Name>
<ram:SpecifiedLegalOrganization>
<ram:ID schemeID="0002">{{ client_siret }}</ram:ID>
</ram:SpecifiedLegalOrganization>
</ram:BuyerTradeParty>
{% comment %}
BuyerOrderReferencedDocument
CHORUS PRO : pour le secteur public, il s'agit de "l'Engagement Juridique". Il est obligatoire pour certains acheteurs. Il convient de se référer à l'annuaire Chorus Pro pour identifier ces acheteurs.
{% endcomment %}
{% if numero_engagement %}
<ram:BuyerOrderReferencedDocument >
<ram:IssuerAssignedID>{{ numero_engagement }}</ram:IssuerAssignedID>
</ram:BuyerOrderReferencedDocument>
{% endif %}
{% if numero_marche %}
<ram:ContractReferencedDocument>
<ram:IssuerAssignedID>{{ numero_marche }}</ram:IssuerAssignedID>
</ram:ContractReferencedDocument>
{% endif %}
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery/>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:TaxBasisTotalAmount>{{ montant_ht }}</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">{{ montant_tva }}</ram:TaxTotalAmount>
<ram:GrandTotalAmount>{{ montant_ttc }}</ram:GrandTotalAmount>
<ram:DuePayableAmount>{{ montant_ttc }}</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>

View File

@ -0,0 +1,59 @@
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
def validate_luhn(string_value, length=None):
'''Verify Luhn checksum on a string representing a number'''
if not string_value:
return False
if length is not None and len(string_value) != length:
return False
if not string_value.isdigit():
return False
# take all digits counting from the right, double value for digits pair
# index (counting from 1), if double has 2 digits take their sum
checksum = 0
for i, x in enumerate(reversed(string_value)):
if i % 2 == 0:
checksum += int(x)
else:
checksum += sum(int(y) for y in str(2 * int(x)))
if checksum % 10 != 0:
return False
return True
def validate_siren(string_value):
if not validate_luhn(string_value, length=9):
raise ValidationError(_('invalid SIREN'))
def validate_siret(string_value):
# special case : La Poste
def helper():
if not string_value.isdigit():
return False
if (string_value.startswith('356000000')
and len(string_value) == 14
and sum(int(x) for x in string_value) % 5 == 0):
return True
return validate_luhn(string_value, length=14)
if not helper():
raise ValidationError(_('invalid SIRET'))

View File

@ -1,5 +1,5 @@
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
# barbacompta - invoicing for dummies
# Copyright (C) 2010-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
@ -15,54 +15,36 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import cgi
import json
import os.path
from django import http
from django.shortcuts import render
from django.template.loader import get_template
from django.shortcuts import get_object_or_404
from django.conf import settings
from weasyprint import HTML
from . import models
def render_to_pdf(template_src, context_dict):
template = get_template(template_src)
html = HTML(string=template.render(context_dict))
try:
pdf = html.write_pdf()
if hasattr(settings, 'FACTURE_DIR'):
facture = context_dict['facture']
filename = os.path.join(
settings.FACTURE_DIR,
'%s-%s.pdf' % (facture.code(), facture.contrat.client.nom.encode('utf8')),
)
with open(filename, 'wb') as fd:
fd.write(pdf)
return http.HttpResponse(pdf, content_type='application/pdf')
except IOError:
return http.HttpResponse('We had some errors<pre>%s</pre>' % cgi.escape(html))
from .models import Contrat, Facture
def facture(request, facture):
context = {'facture': models.Facture.objects.get(id=facture)}
response = render(request, 'facture.html', context=context)
return response
return http.Response(get_object_or_404(Facture, id=facture).html(base_uri=request.build_absolute_uri('/')))
def facture_pdf(request, facture):
context = {'facture': models.Facture.objects.get(id=facture), 'base_uri': request.build_absolute_uri('/')}
response = render_to_pdf('facture.html', context)
return response
pdf = get_object_or_404(Facture, id=facture).pdf(base_uri=request.build_absolute_uri('/'))
if hasattr(settings, 'FACTURE_DIR'):
filename = os.path.join(
settings.FACTURE_DIR,
'%s-%s.pdf' % (facture.code(), facture.contrat.client.nom.encode('utf8')),
)
with open(filename, 'wb') as fd:
fd.write(pdf)
return http.HttpResponse(pdf, content_type='application/pdf')
def api_references(request):
data = []
for contract in (
models.Contrat.objects.exclude(public_description='')
Contrat.objects.exclude(public_description='')
.exclude(client__picture__isnull=True)
.exclude(client__picture='')
):

View File

@ -1,4 +1,5 @@
import os.path
import facturx
# Django settings for facturation project.
@ -43,6 +44,11 @@ LOGGING = {
'handlers': ['console'],
'level': 'INFO',
},
'factur-x': {
'level': 'WARNING',
'handlers': [],
'propagate': False,
}
},
}

BIN
tests/fake-invoice.pdf Normal file

Binary file not shown.

118
tests/test_facturx.py Normal file
View File

@ -0,0 +1,118 @@
# barbacompta - invoicing for dummies
# Copyright (C) 2010-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import io
import pytest
import xml.etree.ElementTree as ET
import facturx
from eo_gestion.eo_facture.facturx import to_pdfa, add_facturx_from_bytes
@pytest.fixture
def fake_invoice_bytes():
with open('tests/fake-invoice.pdf', 'rb') as fd:
return fd.read()
def test_to_pdfa(fake_invoice_bytes):
to_pdfa(fake_invoice_bytes)
def test_add_facturx_from_bytes(fake_invoice_bytes):
facturx_ctx = {
'numero_de_facture': 'F20190001',
'type_facture': '380',
'date_emission': datetime.date(2019, 1, 1),
'chorus_service_code': 'service-code',
'vendeur': 'Entr\'ouvert',
'vendeur_siret': '44317013900036',
'vendeur_tvai': 'FR09491081899',
'client_siret': '1234',
'client_name': 'RGFIPD',
'numero_engagement': '5678',
'numero_marche': 'ABCD',
'montant_ht': '10',
'montant_ttc': '12',
'montant_tva': '2',
}
facturx_bytes = add_facturx_from_bytes(fake_invoice_bytes, facturx_ctx)
xml_filename, xml_str = facturx.get_facturx_xml_from_pdf(io.BytesIO(facturx_bytes))
root = ET.fromstring(xml_str)
def helper(root):
tag = root.tag.split('}')[-1]
if len(root) == 0:
return [tag, root.text or '']
else:
return [tag] + [helper(node) for node in root]
assert helper(root) == [
'CrossIndustryInvoice',
[
'ExchangedDocumentContext',
['BusinessProcessSpecifiedDocumentContextParameter', ['ID', 'A1']],
['GuidelineSpecifiedDocumentContextParameter',
['ID', 'urn:factur-x.eu:1p0:minimum']]
],
[
'ExchangedDocument',
['ID', 'F20190001'],
['TypeCode', '380'],
['IssueDateTime', ['DateTimeString', '20190101']]
],
[
'SupplyChainTradeTransaction',
[
'ApplicableHeaderTradeAgreement',
['BuyerReference', 'service-code'],
[
'SellerTradeParty',
['Name', "Entr'ouvert"],
[
'SpecifiedLegalOrganization',
['ID', '44317013900036']
],
['PostalTradeAddress', ['CountryID', 'FR']],
['SpecifiedTaxRegistration', ['ID', 'FR09491081899']]
],
[
'BuyerTradeParty',
['Name', 'RGFIPD'],
[
'SpecifiedLegalOrganization',
['ID', '1234'],
],
],
['BuyerOrderReferencedDocument', ['IssuerAssignedID', '5678']],
['ContractReferencedDocument', ['IssuerAssignedID', 'ABCD']]
],
['ApplicableHeaderTradeDelivery', ''],
[
'ApplicableHeaderTradeSettlement',
['InvoiceCurrencyCode', 'EUR'],
[
'SpecifiedTradeSettlementHeaderMonetarySummation',
['TaxBasisTotalAmount', '10'],
['TaxTotalAmount', '2'],
['GrandTotalAmount', '12'],
['DuePayableAmount', '12']
]
]
]
]