X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FDB%2FHelper%2FZUGFeRD.pm;h=5455398cd9c948464521d1c0b1bf07474e93eba9;hb=ec4cffa8a9fe604ecaab2a0af734b5e28c040935;hp=f25eddf62776c64991acc34d2b568d4d10d9dd6a;hpb=cf0455f57e7fe398b59a3c4a01f459d16b9db419;p=kivitendo-erp.git diff --git a/SL/DB/Helper/ZUGFeRD.pm b/SL/DB/Helper/ZUGFeRD.pm index f25eddf62..5455398cd 100644 --- a/SL/DB/Helper/ZUGFeRD.pm +++ b/SL/DB/Helper/ZUGFeRD.pm @@ -6,16 +6,19 @@ use utf8; use parent qw(Exporter); our @EXPORT = qw(create_zugferd_data create_zugferd_xmp_data); +use SL::DB::BankAccount; +use SL::DB::GenericTranslation; use SL::DB::Tax; use SL::DB::TaxKey; use SL::Helper::ISO3166; use SL::Helper::ISO4217; use SL::Helper::UNECERecommendation20; +use SL::VATIDNr; use Carp; use Encode qw(encode); -use List::MoreUtils qw(pairwise); -use List::Util qw(sum); +use List::MoreUtils qw(any pairwise); +use List::Util qw(first sum); use Template; use XML::Writer; @@ -49,7 +52,10 @@ sub _type_code { # 381 (Credit note) # 389 (Credit note, self billed invoice) - return $type eq 'credit_note' ? 381 : 380; + return $type eq 'credit_note' ? 381 + : $type eq 'invoice_storno' ? 457 + : $type eq 'credit_note_storno' ? 458 + : 380; } sub _unit_code { @@ -144,7 +150,7 @@ sub _line_item { # $params{xml}->startTag("ram:SpecifiedLineTradeDelivery"); - $params{xml}->dataElement("ram:BilledQuantity", $params{item}->qty, "unitCode" => _unit_code($params{item}->unit)); + $params{xml}->dataElement("ram:BilledQuantity", $params{item}->qty, unitCode => _unit_code($params{item}->unit)); $params{xml}->endTag; # @@ -172,6 +178,28 @@ sub _line_item { # } +sub _specified_trade_settlement_payment_means { + my ($self, %params) = @_; + + # + $params{xml}->startTag('ram:SpecifiedTradeSettlementPaymentMeans'); + $params{xml}->dataElement('ram:TypeCode', $self->direct_debit ? 59 : 58); # 59 = SEPA direct debit, 58 = SEPA credit transfer + + if ($self->direct_debit) { + $params{xml}->startTag('ram:PayerPartyDebtorFinancialAccount'); + $params{xml}->dataElement('ram:IBANID', $self->customer->iban); + $params{xml}->endTag; + + } else { + $params{xml}->startTag('ram:PayeePartyCreditorFinancialAccount'); + $params{xml}->dataElement('ram:IBANID', $params{bank_account}->iban); + $params{xml}->endTag; + } + + $params{xml}->endTag; + # +} + sub _taxes { my ($self, %params) = @_; @@ -186,7 +214,6 @@ sub _taxes { $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) { @@ -195,7 +222,7 @@ sub _taxes { # $params{xml}->startTag("ram:ApplicableTradeTax"); - $params{xml}->dataElement("ram:CalculatedAmount", _r2($params{ptc_data}->{taxes}->{$info->{tax}->{chart_id}})); + $params{xml}->dataElement("ram:CalculatedAmount", _r2($params{ptc_data}->{taxes_by_tax_id}->{$info->{taxkey}->tax_id})); $params{xml}->dataElement("ram:TypeCode", "VAT"); $params{xml}->dataElement("ram:BasisAmount", _r2($info->{linetotal})); $params{xml}->dataElement("ram:CategoryCode", $tax_info{code}); @@ -205,7 +232,7 @@ sub _taxes { } } -sub _format_payment_terms_description { +sub _calculate_payment_terms_values { my ($self) = @_; my (%vars, %amounts, %formatted_amounts); @@ -239,9 +266,19 @@ sub _format_payment_terms_description { $formatted_amounts{$_} = $::form->format_amount(\%::myconfig, $amounts{$_}, 2); } + return ( + vars => \%vars, + amounts => \%amounts, + formatted_amounts => \%formatted_amounts, + ); +} + +sub _format_payment_terms_description { + my ($self, %params) = @_; + 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; + $description =~ s{<\%$_\%>}{ $params{vars}->{$_} }ge for keys %{ $params{vars} }; + $description =~ s{<\%$_\%>}{ $params{formatted_amounts}->{$_} }ge for keys %{ $params{formatted_amounts} }; return $description; } @@ -251,21 +288,26 @@ sub _payment_terms { return unless $self->payment_terms; + my %payment_terms_vars = _calculate_payment_terms_values($self); + # $params{xml}->startTag("ram:SpecifiedTradePaymentTerms"); - $params{xml}->dataElement("ram:Description", _u8(_format_payment_terms_description($self))); + $params{xml}->dataElement("ram:Description", _u8(_format_payment_terms_description($self, %payment_terms_vars))); # $params{xml}->startTag("ram:DueDateDateTime"); - $params{xml}->dataElement("udt:DateTimeString", $self->duedate->strftime('%Y%m%d'), "format" => "102"); + $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) { + my $currency_id = _u8(SL::Helper::ISO4217::map_currency_name_to_code($self->currency->name) // 'EUR'); + # $params{xml}->startTag("ram:ApplicableTradePaymentDiscountTerms"); - $params{xml}->dataElement("ram:BasisPeriodMeasure", $self->payment_terms->terms_skonto, "unitCode" => "DAY"); + $params{xml}->dataElement("ram:BasisPeriodMeasure", $self->payment_terms->terms_skonto, unitCode => "DAY"); + $params{xml}->dataElement("ram:BasisAmount", _r2($payment_terms_vars{amounts}->{invtotal}), currencyID => $currency_id); $params{xml}->dataElement("ram:CalculationPercent", _r2($self->payment_terms->percent_skonto * 100)); $params{xml}->endTag; # @@ -283,7 +325,7 @@ sub _totals { $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:TaxTotalAmount", _r2(sum(values %{ $params{ptc_data}->{taxes_by_tax_id} })), 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)); @@ -297,9 +339,12 @@ sub _exchanged_document_context { # $params{xml}->startTag("rsm:ExchangedDocumentContext"); - $params{xml}->startTag("ram:TestIndicator"); - $params{xml}->dataElement("udt:Indicator", "true"); # TODO: change to 'false' - $params{xml}->endTag; + + if ($self->customer->create_zugferd_invoices_for_this_customer == 2) { + $params{xml}->startTag("ram:TestIndicator"); + $params{xml}->dataElement("udt:Indicator", "true"); + $params{xml}->endTag; + } $params{xml}->startTag("ram:GuidelineSpecifiedDocumentContextParameter"); $params{xml}->dataElement("ram:ID", "urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p0:extended"); @@ -308,6 +353,14 @@ sub _exchanged_document_context { # } +sub _included_note { + my ($self, %params) = @_; + + $params{xml}->startTag("ram:IncludedNote"); + $params{xml}->dataElement("ram:Content", _u8($params{note})); + $params{xml}->endTag; +} + sub _exchanged_document { my ($self, %params) = @_; @@ -320,7 +373,7 @@ sub _exchanged_document { # $params{xml}->startTag("ram:IssueDateTime"); - $params{xml}->dataElement("udt:DateTimeString", $self->transdate->strftime('%Y%m%d'), "format" => "102"); + $params{xml}->dataElement("udt:DateTimeString", $self->transdate->strftime('%Y%m%d'), format => "102"); $params{xml}->endTag; # @@ -328,23 +381,41 @@ sub _exchanged_document { $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 $std_notes = SL::DB::Manager::GenericTranslation->get_all( + where => [ + translation_type => 'ZUGFeRD/notes', + or => [ + language_id => undef, + language_id => $self->language_id, + ], + '!translation' => undef, + '!translation' => '', + ], + ); + + my $std_note = first { $_->language_id == $self->language_id } @{ $std_notes }; + $std_note //= first { !defined $_->language_id } @{ $std_notes }; my $notes = $self->notes_as_stripped_html; - if ($notes) { - $params{xml}->startTag("ram:IncludedNote"); - $params{xml}->dataElement("ram:Content", _u8($notes)); - $params{xml}->endTag; - } + + _included_note($self, %params, note => $self->transaction_description) if $self->transaction_description; + _included_note($self, %params, note => $notes) if $notes; + _included_note($self, %params, note => $std_note->translation) if $std_note; $params{xml}->endTag; # } +sub _specified_tax_registration { + my ($ustid_nr, %params) = @_; + + # + $params{xml}->startTag("ram:SpecifiedTaxRegistration"); + $params{xml}->dataElement("ram:ID", _u8(SL::VATIDNr->normalize($ustid_nr)), schemeID => "VA"); + $params{xml}->endTag; + # +} + sub _seller_trade_party { my ($self, %params) = @_; @@ -364,7 +435,7 @@ sub _seller_trade_party { # $params{xml}->startTag("ram:DefinedTradeContact"); - $params{xml}->dataElement("ram:PersonName", _u8($sales_person_cfg{name} || $sales_person_cfg{login})); + $params{xml}->dataElement("ram:PersonName", _u8($sales_person->safe_name)); if ($sales_person_cfg{tel}) { $params{xml}->startTag("ram:TelephoneUniversalCommunication"); @@ -391,15 +462,7 @@ sub _seller_trade_party { # } - 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; - # - } + _specified_tax_registration($::instance_conf->get_co_ustid, %params); $params{xml}->endTag; # @@ -414,6 +477,7 @@ sub _buyer_trade_party { $params{xml}->dataElement("ram:Name", _u8($self->customer->name)); _customer_postal_trade_address(%params, customer => $self->customer); + _specified_tax_registration($self->customer->ustid, %params) if $self->customer->ustid; $params{xml}->endTag; # @@ -459,7 +523,7 @@ sub _applicable_header_trade_delivery { $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}->dataElement("udt:DateTimeString", ($self->deliverydate // $self->transdate)->strftime('%Y%m%d'), format => "102"); $params{xml}->endTag; $params{xml}->endTag; @@ -475,6 +539,7 @@ sub _applicable_header_trade_settlement { $params{xml}->startTag("ram:ApplicableHeaderTradeSettlement"); $params{xml}->dataElement("ram:InvoiceCurrencyCode", _u8(SL::Helper::ISO4217::map_currency_name_to_code($self->currency->name) // 'EUR')); + _specified_trade_settlement_payment_means($self, %params); _taxes($self, %params); _payment_terms($self, %params); _totals($self, %params); @@ -498,39 +563,88 @@ sub _supply_chain_trade_transaction { # } -sub create_zugferd_data { +sub _validate_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 %result; + my $prefix = $::locale->text('The ZUGFeRD invoice data cannot be generated because the data validation failed.') . ' '; + + if (!$::instance_conf->get_co_ustid) { + SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The VAT registration number is missing in the client configuration.')); + } + + if (!SL::VATIDNr->validate($::instance_conf->get_co_ustid)) { + SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text("The VAT ID number in the client configuration is invalid.")); + } + + if (!$::instance_conf->get_company || any { my $get = "get_address_$_"; !$::instance_conf->$get } qw(street1 zipcode city)) { + SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The company\'s address information is incomplete in the client configuration.')); + } + + if ($::instance_conf->get_address_country && !SL::Helper::ISO3166::map_name_to_alpha_2_code($::instance_conf->get_address_country)) { + SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The country from the company\'s address in the client configuration cannot be mapped to an ISO 3166-1 alpha 2 code.')); + } + + if ($self->customer->country && !SL::Helper::ISO3166::map_name_to_alpha_2_code($self->customer->country)) { + SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The country from the customer\'s address cannot be mapped to an ISO 3166-1 alpha 2 code.')); + } + + if (!SL::Helper::ISO4217::map_currency_name_to_code($self->currency->name)) { + SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The currency "#1" cannot be mapped to an ISO 4217 currency code.', $self->currency->name)); + } + + my $failed_unit = first { !SL::Helper::UNECERecommendation20::map_name_to_code($_) } map { $_->unit } @{ $self->items }; + if ($failed_unit) { + SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('One of the units used (#1) cannot be mapped to a known unit code from the UN/ECE Recommendation 20 list.', $failed_unit)); + } + + if ($self->direct_debit) { + if (!$self->customer->iban) { + SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('The customer\'s bank account number (IBAN) is missing.')); + } + + } else { + my $bank_accounts = SL::DB::Manager::BankAccount->get_all; + $result{bank_account} = scalar(@{ $bank_accounts }) == 1 ? $bank_accounts->[0] : first { $_->use_for_zugferd } @{ $bank_accounts }; - my %params = ( - ptc_data => \%ptc_data, - xml => $xml, + if (!$result{bank_account}) { + SL::X::ZUGFeRDValidation->throw(message => $prefix . $::locale->text('No bank account flagged for ZUGFeRD usage was found.')); + } + } + + return %result; +} + +sub create_zugferd_data { + my ($self) = @_; + + my $output = ''; + + my %params = _validate_data($self); + $params{ptc_data} = { $self->calculate_prices_and_taxes }; + $params{xml} = XML::Writer->new( + OUTPUT => \$output, + DATA_MODE => 1, + DATA_INDENT => 2, + ENCODING => 'utf-8', ); - $xml->xmlDecl(); + $params{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"); + $params{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; + $params{xml}->endTag; # return $output;