From: Moritz Bunkus Date: Fri, 28 Feb 2020 11:03:39 +0000 (+0100) Subject: ZUGFeRD: Rechnungen mit ZUGFeRD-Daten erzeugen X-Git-Tag: release-3.5.6.1~245^2~30 X-Git-Url: http://wagnertech.de/git?a=commitdiff_plain;h=cf0455f57e7fe398b59a3c4a01f459d16b9db419;p=kivitendo-erp.git ZUGFeRD: Rechnungen mit ZUGFeRD-Daten erzeugen --- diff --git a/SL/DB/Helper/PDF_A.pm b/SL/DB/Helper/PDF_A.pm index f3388a336..99ee8d408 100644 --- a/SL/DB/Helper/PDF_A.pm +++ b/SL/DB/Helper/PDF_A.pm @@ -57,6 +57,12 @@ sub create_pdf_a_print_options { author => $author, language => $pdf_language, }, + zugferd => { + conformance_level => 'EXTENDED', + document_file_name => 'ZUGFeRD-invoice.xml', + document_type => 'INVOICE', + version => '1.0', + }, ), }; } diff --git a/SL/DB/Helper/ZUGFeRD.pm b/SL/DB/Helper/ZUGFeRD.pm new file mode 100644 index 000000000..f25eddf62 --- /dev/null +++ b/SL/DB/Helper/ZUGFeRD.pm @@ -0,0 +1,550 @@ +package SL::DB::Helper::ZUGFeRD; + +use strict; +use utf8; + +use parent qw(Exporter); +our @EXPORT = qw(create_zugferd_data create_zugferd_xmp_data); + +use SL::DB::Tax; +use SL::DB::TaxKey; +use SL::Helper::ISO3166; +use SL::Helper::ISO4217; +use SL::Helper::UNECERecommendation20; + +use Carp; +use Encode qw(encode); +use List::MoreUtils qw(pairwise); +use List::Util qw(sum); +use Template; +use XML::Writer; + +my @line_names = qw(LineOne LineTwo LineThree); + +sub _u8 { + my ($value) = @_; + return encode('UTF-8', $value // ''); +} + +sub _r2 { + my ($value) = @_; + return $::form->round_amount($value, 2); +} + +sub _type_name { + my ($self) = @_; + my $type = $self->invoice_type; + + no warnings 'once'; + return $type eq 'ar_transaction' ? $::locale->text('Invoice') : $self->displayable_type; +} + +sub _type_code { + my ($self) = @_; + my $type = $self->invoice_type; + + # 326 (Partial invoice) + # 380 (Commercial invoice) + # 384 (Corrected Invoice) + # 381 (Credit note) + # 389 (Credit note, self billed invoice) + + return $type eq 'credit_note' ? 381 : 380; +} + +sub _unit_code { + my ($unit) = @_; + + # Mapping from kivitendo's units to UN/ECE Recommendation 20 & 21. + my $code = SL::Helper::UNECERecommendation20::map_name_to_code($unit); + return $code if $code; + + $::lxdebug->message(LXDebug::WARN(), "ZUGFeRD unit name mapping: no UN/ECE Recommendation 20/21 unit known for kivitendo unit '$unit'; using 'C62'"); + + return 'C62'; +} + +sub _parse_our_address { + my @result; + my @street = grep { $_ } ($::instance_conf->get_address_street1, $::instance_conf->get_address_street2); + + push @result, [ 'PostcodeCode', $::instance_conf->get_address_zipcode ] if $::instance_conf->get_address_zipcode; + push @result, grep { $_->[1] } pairwise { [ $a, $b] } @line_names, @street; + push @result, [ 'CityName', $::instance_conf->get_address_city ] if $::instance_conf->get_address_city; + push @result, [ 'CountryID', SL::Helper::ISO3166::map_name_to_alpha_2_code($::instance_conf->get_address_country) // 'DE' ]; + + return @result; +} + +sub _customer_postal_trade_address { + my (%params) = @_; + + # + $params{xml}->startTag("ram:PostalTradeAddress"); + + my @parts = grep { $_ } map { $params{customer}->$_ } qw(department_1 department_2 street); + + $params{xml}->dataElement("ram:PostcodeCode", _u8($params{customer}->zipcode)); + $params{xml}->dataElement("ram:" . $_->[0], _u8($_->[1])) for grep { $_->[1] } pairwise { [ $a, $b] } @line_names, @parts; + $params{xml}->dataElement("ram:CityName", _u8($params{customer}->city)); + $params{xml}->dataElement("ram:CountryID", _u8(SL::Helper::ISO3166::map_name_to_alpha_2_code($params{customer}->country) // 'DE')); + $params{xml}->endTag; + # +} + +sub _tax_rate_and_code { + my ($taxzone, $tax) = @_; + + my ($tax_rate, $tax_code) = @_; + + if ($taxzone->description =~ m{Au.*erhalb}) { + $tax_rate = 0; + $tax_code = 'G'; + + } elsif ($taxzone->description =~ m{EU mit}) { + $tax_rate = 0; + $tax_code = 'K'; + + } else { + $tax_rate = $tax->rate * 100; + $tax_code = !$tax_rate ? 'Z' : 'S'; + } + + return (rate => $tax_rate, code => $tax_code); +} + +sub _line_item { + my ($self, %params) = @_; + + my $item_ptc = $params{ptc_data}->{items}->[$params{line_number}]; + + my $taxkey = $item_ptc->{taxkey_id} ? SL::DB::TaxKey->load_cached($item_ptc->{taxkey_id}) : undef; + my $tax = $item_ptc->{taxkey_id} ? SL::DB::Tax->load_cached($taxkey->tax_id) : undef; + my %tax_info = _tax_rate_and_code($self->taxzone, $tax); + + # + $params{xml}->startTag("ram:IncludedSupplyChainTradeLineItem"); + + # + $params{xml}->startTag("ram:AssociatedDocumentLineDocument"); + $params{xml}->dataElement("ram:LineID", $params{line_number} + 1); + $params{xml}->endTag; + + $params{xml}->startTag("ram:SpecifiedTradeProduct"); + $params{xml}->dataElement("ram:SellerAssignedID", _u8($params{item}->part->partnumber)); + $params{xml}->dataElement("ram:Name", _u8($params{item}->description)); + $params{xml}->endTag; + + $params{xml}->startTag("ram:SpecifiedLineTradeAgreement"); + $params{xml}->startTag("ram:NetPriceProductTradePrice"); + $params{xml}->dataElement("ram:ChargeAmount", _r2($item_ptc->{sellprice})); + $params{xml}->endTag; + $params{xml}->endTag; + # + + # + $params{xml}->startTag("ram:SpecifiedLineTradeDelivery"); + $params{xml}->dataElement("ram:BilledQuantity", $params{item}->qty, "unitCode" => _unit_code($params{item}->unit)); + $params{xml}->endTag; + # + + # + $params{xml}->startTag("ram:SpecifiedLineTradeSettlement"); + + # + $params{xml}->startTag("ram:ApplicableTradeTax"); + $params{xml}->dataElement("ram:TypeCode", "VAT"); + $params{xml}->dataElement("ram:CategoryCode", $tax_info{code}); + $params{xml}->dataElement("ram:RateApplicablePercent", _r2($tax_info{rate})); + $params{xml}->endTag; + # + + # + $params{xml}->startTag("ram:SpecifiedTradeSettlementLineMonetarySummation"); + $params{xml}->dataElement("ram:LineTotalAmount", _r2($item_ptc->{linetotal})); + $params{xml}->endTag; + # + + $params{xml}->endTag; + # + + $params{xml}->endTag; + # +} + +sub _taxes { + my ($self, %params) = @_; + + my %taxkey_info; + + foreach my $item (@{ $params{ptc_data}->{items} }) { + $taxkey_info{$item->{taxkey_id}} //= { + linetotal => 0, + tax_amount => 0, + }; + my $info = $taxkey_info{$item->{taxkey_id}}; + $info->{taxkey} //= SL::DB::TaxKey->load_cached($item->{taxkey_id}); + $info->{tax} //= SL::DB::Tax->load_cached($info->{taxkey}->tax_id); + $info->{linetotal} += $item->{linetotal}; + $info->{tax_amount} += $item->{tax_amount}; + } + + foreach my $taxkey_id (sort keys %taxkey_info) { + my $info = $taxkey_info{$taxkey_id}; + my %tax_info = _tax_rate_and_code($self->taxzone, $info->{tax}); + + # + $params{xml}->startTag("ram:ApplicableTradeTax"); + $params{xml}->dataElement("ram:CalculatedAmount", _r2($params{ptc_data}->{taxes}->{$info->{tax}->{chart_id}})); + $params{xml}->dataElement("ram:TypeCode", "VAT"); + $params{xml}->dataElement("ram:BasisAmount", _r2($info->{linetotal})); + $params{xml}->dataElement("ram:CategoryCode", $tax_info{code}); + $params{xml}->dataElement("ram:RateApplicablePercent", _r2($tax_info{rate})); + $params{xml}->endTag; + # + } +} + +sub _format_payment_terms_description { + my ($self) = @_; + + my (%vars, %amounts, %formatted_amounts); + + local $::myconfig{numberformat} = $::myconfig{numberformat}; + local $::myconfig{dateformat} = $::myconfig{dateformat}; + + if ($self->language_id) { + my $language = SL::DB::Language->load_cached($self->language_id); + $::myconfig{dateformat} = $language->output_dateformat if $language->output_dateformat; + $::myconfig{numberformat} = $language->output_numberformat if $language->output_numberformat; + } + + $vars{currency} = $self->currency->name if $self->currency; + $vars{$_} = $self->customer->$_ for qw(account_number bank bank_code bic iban mandate_date_of_signature mandator_id); + $vars{$_} = $self->payment_terms->$_ for qw(terms_netto terms_skonto percent_skonto); + $vars{payment_description} = $self->payment_terms->description; + $vars{netto_date} = $self->payment_terms->calc_date(reference_date => $self->transdate, due_date => $self->duedate, terms => 'net')->to_kivitendo; + $vars{skonto_date} = $self->payment_terms->calc_date(reference_date => $self->transdate, due_date => $self->duedate, terms => 'discount')->to_kivitendo; + + $amounts{invtotal} = $self->amount; + $amounts{total} = $self->amount - $self->paid; + + $amounts{skonto_in_percent} = 100.0 * $vars{percent_skonto}; + $amounts{skonto_amount} = $amounts{invtotal} * $vars{percent_skonto}; + $amounts{invtotal_wo_skonto} = $amounts{invtotal} * (1 - $vars{percent_skonto}); + $amounts{total_wo_skonto} = $amounts{total} * (1 - $vars{percent_skonto}); + + foreach (keys %amounts) { + $amounts{$_} = $::form->round_amount($amounts{$_}, 2); + $formatted_amounts{$_} = $::form->format_amount(\%::myconfig, $amounts{$_}, 2); + } + + my $description = ($self->payment_terms->translated_attribute('description_long_invoice', $self->language_id) // '') || $self->payment_terms->description_long_invoice; + $description =~ s{<\%$_\%>}{ $vars{$_} }ge for keys %vars; + $description =~ s{<\%$_\%>}{ $formatted_amounts{$_} }ge for keys %formatted_amounts; + + return $description; +} + +sub _payment_terms { + my ($self, %params) = @_; + + return unless $self->payment_terms; + + # + $params{xml}->startTag("ram:SpecifiedTradePaymentTerms"); + + $params{xml}->dataElement("ram:Description", _u8(_format_payment_terms_description($self))); + + # + $params{xml}->startTag("ram:DueDateDateTime"); + $params{xml}->dataElement("udt:DateTimeString", $self->duedate->strftime('%Y%m%d'), "format" => "102"); + $params{xml}->endTag; + # + + if ($self->payment_terms->percent_skonto && $self->payment_terms->terms_skonto) { + # + $params{xml}->startTag("ram:ApplicableTradePaymentDiscountTerms"); + $params{xml}->dataElement("ram:BasisPeriodMeasure", $self->payment_terms->terms_skonto, "unitCode" => "DAY"); + $params{xml}->dataElement("ram:CalculationPercent", _r2($self->payment_terms->percent_skonto * 100)); + $params{xml}->endTag; + # + } + + $params{xml}->endTag; + # +} + +sub _totals { + my ($self, %params) = @_; + + # + $params{xml}->startTag("ram:SpecifiedTradeSettlementHeaderMonetarySummation"); + + $params{xml}->dataElement("ram:LineTotalAmount", _r2($self->netamount)); + $params{xml}->dataElement("ram:TaxBasisTotalAmount", _r2($self->netamount)); + $params{xml}->dataElement("ram:TaxTotalAmount", _r2(sum(values %{ $params{ptc_data}->{taxes} })), "currencyID" => "EUR"); + $params{xml}->dataElement("ram:GrandTotalAmount", _r2($self->amount)); + $params{xml}->dataElement("ram:TotalPrepaidAmount", _r2($self->paid)); + $params{xml}->dataElement("ram:DuePayableAmount", _r2($self->amount - $self->paid)); + + $params{xml}->endTag; + # +} + +sub _exchanged_document_context { + my ($self, %params) = @_; + + # + $params{xml}->startTag("rsm:ExchangedDocumentContext"); + $params{xml}->startTag("ram:TestIndicator"); + $params{xml}->dataElement("udt:Indicator", "true"); # TODO: change to 'false' + $params{xml}->endTag; + + $params{xml}->startTag("ram:GuidelineSpecifiedDocumentContextParameter"); + $params{xml}->dataElement("ram:ID", "urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p0:extended"); + $params{xml}->endTag; + $params{xml}->endTag; + # +} + +sub _exchanged_document { + my ($self, %params) = @_; + + # + $params{xml}->startTag("rsm:ExchangedDocument"); + + $params{xml}->dataElement("ram:ID", _u8($self->invnumber)); + $params{xml}->dataElement("ram:Name", _u8(_type_name($self))); + $params{xml}->dataElement("ram:TypeCode", _u8(_type_code($self))); + + # + $params{xml}->startTag("ram:IssueDateTime"); + $params{xml}->dataElement("udt:DateTimeString", $self->transdate->strftime('%Y%m%d'), "format" => "102"); + $params{xml}->endTag; + # + + if ($self->language && (($self->language->template_code // '') =~ m{^(de|en)}i)) { + $params{xml}->dataElement("ram:LanguageID", uc($1)); + } + + if ($self->transaction_description) { + $params{xml}->startTag("ram:IncludedNote"); + $params{xml}->dataElement("ram:Content", _u8($self->transaction_description)); + $params{xml}->endTag; + } + + my $notes = $self->notes_as_stripped_html; + if ($notes) { + $params{xml}->startTag("ram:IncludedNote"); + $params{xml}->dataElement("ram:Content", _u8($notes)); + $params{xml}->endTag; + } + + $params{xml}->endTag; + # +} + +sub _seller_trade_party { + my ($self, %params) = @_; + + my @our_address = _parse_our_address(); + + my $sales_person = $self->salesman; + my $sales_person_auth = SL::DB::Manager::AuthUser->find_by(login => $sales_person->login); + my %sales_person_cfg = $sales_person_auth ? %{ $sales_person_auth->config_values } : (); + $sales_person_cfg{email} ||= $sales_person->deleted_email; + $sales_person_cfg{tel} ||= $sales_person->deleted_tel; + + # + $params{xml}->startTag("ram:SellerTradeParty"); + $params{xml}->dataElement("ram:ID", _u8($self->customer->c_vendor_id)) if ($self->customer->c_vendor_id // '') ne ''; + $params{xml}->dataElement("ram:Name", _u8($::instance_conf->get_company)); + + # + $params{xml}->startTag("ram:DefinedTradeContact"); + + $params{xml}->dataElement("ram:PersonName", _u8($sales_person_cfg{name} || $sales_person_cfg{login})); + + if ($sales_person_cfg{tel}) { + $params{xml}->startTag("ram:TelephoneUniversalCommunication"); + $params{xml}->dataElement("ram:CompleteNumber", _u8($sales_person_cfg{tel})); + $params{xml}->endTag; + } + + if ($sales_person_cfg{email}) { + $params{xml}->startTag("ram:EmailURIUniversalCommunication"); + $params{xml}->dataElement("ram:URIID", _u8($sales_person_cfg{email})); + $params{xml}->endTag; + } + + $params{xml}->endTag; + # + + if (@our_address) { + # + $params{xml}->startTag("ram:PostalTradeAddress"); + foreach my $element (@our_address) { + $params{xml}->dataElement("ram:" . $element->[0], _u8($element->[1])); + } + $params{xml}->endTag; + # + } + + my $ustid_nr = $::instance_conf->get_co_ustid; + if ($ustid_nr) { + $ustid_nr = "DE$ustid_nr" unless $ustid_nr =~ m{^[A-Z]{2}}; + # + $params{xml}->startTag("ram:SpecifiedTaxRegistration"); + $params{xml}->dataElement("ram:ID", _u8($ustid_nr), "schemeID" => "VA"); + $params{xml}->endTag; + # + } + + $params{xml}->endTag; + # +} + +sub _buyer_trade_party { + my ($self, %params) = @_; + + # + $params{xml}->startTag("ram:BuyerTradeParty"); + $params{xml}->dataElement("ram:ID", _u8($self->customer->customernumber)); + $params{xml}->dataElement("ram:Name", _u8($self->customer->name)); + + _customer_postal_trade_address(%params, customer => $self->customer); + + $params{xml}->endTag; + # +} + +sub _included_supply_chain_trade_line_item { + my ($self, %params) = @_; + + my $line_number = 0; + foreach my $item (@{ $self->items }) { + _line_item($self, %params, item => $item, line_number => $line_number); + $line_number++; + } +} + +sub _applicable_header_trade_agreement { + my ($self, %params) = @_; + + # + $params{xml}->startTag("ram:ApplicableHeaderTradeAgreement"); + + _seller_trade_party($self, %params); + _buyer_trade_party($self, %params); + + if ($self->cusordnumber) { + # + $params{xml}->startTag("ram:BuyerOrderReferencedDocument"); + $params{xml}->dataElement("ram:IssuerAssignedID", _u8($self->cusordnumber)); + $params{xml}->endTag; + # + } + + $params{xml}->endTag; + # +} + +sub _applicable_header_trade_delivery { + my ($self, %params) = @_; + + # + $params{xml}->startTag("ram:ApplicableHeaderTradeDelivery"); + # + $params{xml}->startTag("ram:ActualDeliverySupplyChainEvent"); + + $params{xml}->startTag("ram:OccurrenceDateTime"); + $params{xml}->dataElement("udt:DateTimeString", ($self->deliverydate // $self->transdate)->strftime('%Y%m%d'), "format" => "102"); + $params{xml}->endTag; + + $params{xml}->endTag; + # + $params{xml}->endTag; + # +} + +sub _applicable_header_trade_settlement { + my ($self, %params) = @_; + + # + $params{xml}->startTag("ram:ApplicableHeaderTradeSettlement"); + $params{xml}->dataElement("ram:InvoiceCurrencyCode", _u8(SL::Helper::ISO4217::map_currency_name_to_code($self->currency->name) // 'EUR')); + + _taxes($self, %params); + _payment_terms($self, %params); + _totals($self, %params); + + $params{xml}->endTag; + # +} + +sub _supply_chain_trade_transaction { + my ($self, %params) = @_; + + # + $params{xml}->startTag("rsm:SupplyChainTradeTransaction"); + + _included_supply_chain_trade_line_item($self, %params); + _applicable_header_trade_agreement($self, %params); + _applicable_header_trade_delivery($self, %params); + _applicable_header_trade_settlement($self, %params); + + $params{xml}->endTag; + # +} + +sub create_zugferd_data { + my ($self) = @_; + + my %ptc_data = $self->calculate_prices_and_taxes; + my $output = ''; + my $xml = XML::Writer->new( + OUTPUT => \$output, + DATA_MODE => 1, + DATA_INDENT => 2, + ENCODING => 'utf-8', + ); + + my %params = ( + ptc_data => \%ptc_data, + xml => $xml, + ); + + $xml->xmlDecl(); + + # + $xml->startTag("rsm:CrossIndustryInvoice", + "xmlns:a" => "urn:un:unece:uncefact:data:standard:QualifiedDataType:100", + "xmlns:rsm" => "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100", + "xmlns:qdt" => "urn:un:unece:uncefact:data:standard:QualifiedDataType:10", + "xmlns:ram" => "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100", + "xmlns:xs" => "http://www.w3.org/2001/XMLSchema", + "xmlns:udt" => "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"); + + _exchanged_document_context($self, %params); + _exchanged_document($self, %params); + _supply_chain_trade_transaction($self, %params); + + $xml->endTag; + # + + return $output; +} + +sub create_zugferd_xmp_data { + my ($self) = @_; + + return { + conformance_level => 'EXTENDED', + document_file_name => 'ZUGFeRD-invoice.xml', + document_type => 'INVOICE', + version => '1.0', + }; +} + +1; diff --git a/SL/DB/Invoice.pm b/SL/DB/Invoice.pm index 673fae909..f33d75dca 100644 --- a/SL/DB/Invoice.pm +++ b/SL/DB/Invoice.pm @@ -17,6 +17,7 @@ use SL::DB::Helper::PDF_A; use SL::DB::Helper::PriceTaxCalculator; use SL::DB::Helper::PriceUpdater; use SL::DB::Helper::TransNumberGenerator; +use SL::DB::Helper::ZUGFeRD; use SL::Locale::String qw(t8); use SL::DB::CustomVariable; diff --git a/bin/mozilla/io.pl b/bin/mozilla/io.pl index f8c660d2a..1ee0d2c09 100644 --- a/bin/mozilla/io.pl +++ b/bin/mozilla/io.pl @@ -1288,6 +1288,10 @@ sub print_form { $form->{TEMPLATE_DRIVER_OPTIONS}->{variable_content_types} = $form->get_variable_content_types(); } + if ($form->{format} =~ m{pdf}) { + _maybe_attach_zugferd_data($form); + } + $form->isblank("email", $locale->text('E-mail address missing!')) if ($form->{media} eq 'email'); $form->isblank("${inv}date", @@ -2110,3 +2114,25 @@ sub send_sales_purchase_email { print $::form->redirect_header($script . '?action=edit&id=' . $::form->escape($id) . '&type=' . $::form->escape($type)); } + +sub _maybe_attach_zugferd_data { + my ($form) = @_; + + my $record = _make_record(); + + return if !$record || !$record->can('create_pdf_a_print_options') || !$record->can('create_zugferd_data'); + + my $xmlfile = File::Temp->new; + $xmlfile->print($record->create_zugferd_data); + $xmlfile->close; + + $form->{TEMPLATE_DRIVER_OPTIONS}->{pdf_a} = $record->create_pdf_a_print_options(zugferd_xmp_data => $record->create_zugferd_xmp_data); + $form->{TEMPLATE_DRIVER_OPTIONS}->{pdf_attachments} = [ + { source => $xmlfile, + name => 'ZUGFeRD-invoice.xml', + description => $::locale->text('ZUGFeRD invoice'), + relationship => '/Alternative', + mime_type => 'text/xml', + } + ]; +} diff --git a/locale/de/all b/locale/de/all index 79a155959..0567e1f14 100755 --- a/locale/de/all +++ b/locale/de/all @@ -3978,6 +3978,7 @@ $self->{texts} = { 'Your download does not exist anymore. Please re-run the DATEV export assistant.' => 'Ihr Download existiert nicht mehr. Bitte starten Sie den DATEV-Exportassistenten erneut.', 'Your import is being processed.' => 'Ihr Import wird verarbeitet', 'Your target quantity will be added to the stocked quantity.' => 'Ihre gezählte Zielmenge wird zum Lagerbestand hinzugezählt.', + 'ZUGFeRD invoice' => 'ZUGFeRD-Rechnung', 'Zeitraum' => 'Zeitraum', 'Zero amount posting!' => 'Buchung ohne Wert', 'Zip' => 'PLZ', diff --git a/templates/pdf/pdf_a_metadata.xmp b/templates/pdf/pdf_a_metadata.xmp index a26157b2e..a31dfdb16 100644 --- a/templates/pdf/pdf_a_metadata.xmp +++ b/templates/pdf/pdf_a_metadata.xmp @@ -1,6 +1,7 @@ - + +[% IF zugferd %] + + ZUGFeRD PDFA Extension Schema + urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0# + fx + + + + DocumentFileName + Text + external + name of the embedded XML invoice file + + + DocumentType + Text + external + INVOICE + + + Version + Text + external + The actual version of the ZUGFeRD data + + + ConformanceLevel + Text + external + The conformance level of the ZUGFeRD data + + + + +[% END %] @@ -86,6 +122,16 @@ + +[% IF zugferd %] + +[% END %] +