use parent qw(Exporter);
our @EXPORT = qw(create_zugferd_data create_zugferd_xmp_data);
+use SL::DB::GenericTranslation;
use SL::DB::Tax;
use SL::DB::TaxKey;
use SL::Helper::ISO3166;
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;
# 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 {
# <rsm:ExchangedDocumentContext>
$params{xml}->startTag("rsm:ExchangedDocumentContext");
- $params{xml}->startTag("ram:TestIndicator");
- $params{xml}->dataElement("udt:Indicator", "true"); # TODO: change to 'false'
- $params{xml}->endTag;
+
+ if ($::instance_conf->get_create_zugferd_invoices == 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");
# </rsm:ExchangedDocumentContext>
}
+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) = @_;
$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;
# </rsm:ExchangedDocument>
}
+sub _specified_tax_registration {
+ my ($ustid_nr, %params) = @_;
+
+ return unless $ustid_nr;
+
+ $ustid_nr = "DE$ustid_nr" unless $ustid_nr =~ m{^[A-Z]{2}};
+
+ # <ram:SpecifiedTaxRegistration>
+ $params{xml}->startTag("ram:SpecifiedTaxRegistration");
+ $params{xml}->dataElement("ram:ID", _u8($ustid_nr), "schemeID" => "VA");
+ $params{xml}->endTag;
+ # </ram:SpecifiedTaxRegistration>
+}
+
sub _seller_trade_party {
my ($self, %params) = @_;
# </ram:PostalTradeAddress>
}
- my $ustid_nr = $::instance_conf->get_co_ustid;
- if ($ustid_nr) {
- $ustid_nr = "DE$ustid_nr" unless $ustid_nr =~ m{^[A-Z]{2}};
- # <ram:SpecifiedTaxRegistration>
- $params{xml}->startTag("ram:SpecifiedTaxRegistration");
- $params{xml}->dataElement("ram:ID", _u8($ustid_nr), "schemeID" => "VA");
- $params{xml}->endTag;
- # </ram:SpecifiedTaxRegistration>
- }
+ _specified_tax_registration($::instance_conf->get_co_ustid, %params);
$params{xml}->endTag;
# </ram:SellerTradeParty>
$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;
# </ram:BuyerTradeParty>
# </rsm:SupplyChainTradeTransaction>
}
+sub _validate_data {
+ my ($self) = @_;
+
+ 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 (!$::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));
+ }
+}
+
sub create_zugferd_data {
my ($self) = @_;
+ _validate_data($self);
+
my %ptc_data = $self->calculate_prices_and_taxes;
my $output = '';
my $xml = XML::Writer->new(