X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;ds=sidebyside;f=SL%2FDB%2FHelper%2FZUGFeRD.pm;h=211e88535e0e3f2508b02f8d9dbad0877d004797;hb=a805ddafe2970ba5c07a59d90749fa0c7e24f903;hp=f25eddf62776c64991acc34d2b568d4d10d9dd6a;hpb=cf0455f57e7fe398b59a3c4a01f459d16b9db419;p=kivitendo-erp.git
diff --git a/SL/DB/Helper/ZUGFeRD.pm b/SL/DB/Helper/ZUGFeRD.pm
index f25eddf62..211e88535 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) = @_;
@@ -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);
$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;