Skip to content

Commit

Permalink
XMLInvoice: gültige namespaces aus der xml holen ...
Browse files Browse the repository at this point in the history
Die namespaces CrossIndustryInvoice, ReusableAggregateBusinessInformationEntity, UnqualifiedDataType
können beliebig sein und sind von ZUGfERD nicht festgelegt.
Deswegen werden die ns jetzt vorher ausgelesen.
  • Loading branch information
wernerhahn committed Jan 11, 2025
1 parent 48fa6dc commit f9422f0
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 50 deletions.
7 changes: 5 additions & 2 deletions SL/XMLInvoice.pm
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ sub new {
return $self;
}

# Determine parser class to use
# Determine parser class and namespaces to use
my $type = first {
$_->check_signature($self->{dom})
} @document_modules;
Expand All @@ -79,9 +79,12 @@ sub new {
);
return $self;
}

bless $self, $type;

my $namespaces = $self->namespaces($self->{dom});

$self->{namespaces} = $namespaces;

# Implementation sanity check for child classes: make sure they are aware of
# the keys the hash returned by their metadata() method must contain.
my @missing_data_keys = grep { !${$self->_data_keys}{$_} } @{ $self->data_keys };
Expand Down
14 changes: 14 additions & 0 deletions SL/XMLInvoice/Base.pm
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ to discover the metadata keys guaranteed to be present.
=cut

sub data_keys {
my $self = shift;
my @keys = (
'currency', # The bill's currency, such as "EUR"
'direct_debit', # Boolean: whether the bill will get paid by direct debit (1) or not (0)
Expand Down Expand Up @@ -85,6 +86,7 @@ to discover the metadata keys guaranteed to be present.
=cut

sub item_keys {
my $self = shift;
my @keys = (
'currency',
'description',
Expand Down Expand Up @@ -144,6 +146,17 @@ sub check_signature {
die "Children of $self must implement a check_signature() method returning 1 for supported XML, 0 for unsupported XML.";
}

=item namespaces($dom)
This static method takes a DOM object and returns an ArrayofHashes[ data => localname ]. C<SL::XMLInvoice> uses this method to determine which ns is valid for wich data. All child classes must implement this method.
=cut

sub namespaces {
my $self = shift;
die "Children of $self must implement a namespaces() method returning an aoh with the namespaces";
}

=item supported()
This static method returns an array of free-form strings describing XML invoice
Expand Down Expand Up @@ -227,6 +240,7 @@ C<item_keys>. Omitting this method from a child class will cause an exception.
=head1 AUTHOR
Johannes Grassler <info@computer-grassler.de>
Werner Hahn <wh@futureworldsearch.net>
=cut

Expand Down
92 changes: 67 additions & 25 deletions SL/XMLInvoice/CrossIndustryDocument.pm
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ use warnings;

use parent qw(SL::XMLInvoice::Base);

use constant ITEMS_XPATH => '//ram:IncludedSupplyChainTradeLineItem';

=head1 NAME
SL::XMLInvoice::CrossIndustryDocument - XML parser for UN/CEFACT Cross Industry Document
Expand Down Expand Up @@ -51,6 +49,7 @@ returned by the C<items()> method.
=head1 AUTHOR
Johannes Grassler <info@computer-grassler.de>
Werner Hahn <wh@futureworldsearch.net>
=cut

Expand All @@ -73,39 +72,75 @@ sub check_signature {
return 0;
}

sub namespaces {
my ($self, $dom) = @_;
my $rootnode = $dom->documentElement;
my @nodes = $rootnode->findnodes('namespace::*');
my @namespaces = map {[ $_->getData, $_->getLocalName]} @nodes;
return \@namespaces;
}

# XML XPath expressions for global metadata
sub scalar_xpaths {
my ($self) = @_;

my $rsm = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'};
my $ram = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'};
my $udt = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'};
$ram .= ":" if $ram;
$rsm .= ":" if $rsm;
$udt .= ":" if $udt;

return {
currency => ['//ram:InvoiceCurrencyCode'],
direct_debit => ['//ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode'],
duedate => ['//ram:DueDateDateTime/udt:DateTimeString', '//ram:EffectiveSpecifiedPeriod/ram:CompleteDateTime/udt:DateTimeString'],
gross_total => ['//ram:DuePayableAmount'],
iban => ['//ram:SpecifiedTradeSettlementPaymentMeans/ram:PayeePartyCreditorFinancialAccount/ram:IBANID'],
invnumber => ['//rsm:HeaderExchangedDocument/ram:ID'],
net_total => ['//ram:TaxBasisTotalAmount'],
transdate => ['//ram:IssueDateTime/udt:DateTimeString'],
taxnumber => ['//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]'],
type => ['//rsm:HeaderExchangedDocument/ram:TypeCode'],
ustid => ['//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]'],
vendor_name => ['//ram:SellerTradeParty/ram:Name'],
currency => ['//' . $ram . 'InvoiceCurrencyCode'],
direct_debit => ['//' . $ram . 'SpecifiedTradeSettlementPaymentMeans/' . $ram . 'TypeCode'],
duedate => ['//' . $ram . 'DueDateDateTime/' . $udt . 'DateTimeString', '//' . $ram . 'EffectiveSpecifiedPeriod/' . $ram . 'CompleteDateTime/' . $udt . 'DateTimeString'],
gross_total => ['//' . $ram . 'DuePayableAmount'],
iban => ['//' . $ram . 'SpecifiedTradeSettlementPaymentMeans/' . $ram . 'PayeePartyCreditorFinancialAccount/' . $ram . 'IBANID'],
invnumber => ['//' . $rsm . 'HeaderExchangedDocument/' . $ram . 'ID'],
net_total => ['//' . $ram . 'TaxBasisTotalAmount'],
transdate => ['//' . $ram . 'IssueDateTime/' . $udt . 'DateTimeString'],
taxnumber => ['//' . $ram . 'SellerTradeParty/' . $ram . 'SpecifiedTaxRegistration/' . $ram . 'ID[@schemeID="FC"]'],
type => ['//' . $rsm . 'HeaderExchangedDocument/' . $ram . 'TypeCode'],
ustid => ['//' . $ram . 'SellerTradeParty/' . $ram . 'SpecifiedTaxRegistration/' . $ram . 'ID[@schemeID="VA"]'],
vendor_name => ['//' . $ram . 'SellerTradeParty/' . $ram . 'Name'],
};
}

sub item_xpaths {
my ($self) = @_;

my $rsm = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'};
my $ram = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'};
my $udt = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'};
$ram .= ":" if $ram;
$rsm .= ":" if $rsm;
$udt .= ":" if $udt;

return {
'currency' => ['./ram:SpecifiedSupplyChainTradeAgreement/ram:GrossPriceProductTradePrice/ram:ChargeAmount[attribute::currencyID]',
'./ram:SpecifiedSupplyChainTradeAgreement/ram:GrossPriceProductTradePrice/ram:BasisAmount'],
'price' => ['./ram:SpecifiedSupplyChainTradeAgreement/ram:GrossPriceProductTradePrice/ram:ChargeAmount',
'./ram:SpecifiedSupplyChainTradeAgreement/ram:GrossPriceProductTradePrice/ram:BasisAmount'],
'description' => ['./ram:SpecifiedTradeProduct/ram:Name'],
'quantity' => ['./ram:SpecifiedSupplyChainTradeDelivery/ram:BilledQuantity',],
'subtotal' => ['./ram:SpecifiedSupplyChainTradeSettlement/ram:SpecifiedTradeSettlementMonetarySummation/ram:LineTotalAmount'],
'tax_rate' => ['./ram:SpecifiedSupplyChainTradeSettlement/ram:ApplicableTradeTax/ram:ApplicablePercent'],
'tax_scheme' => ['./ram:SpecifiedSupplyChainTradeSettlement/ram:ApplicableTradeTax/ram:TypeCode'],
'vendor_partno' => ['./ram:SpecifiedTradeProduct/ram:SellerAssignedID'],
'currency' => ['./' . $ram . ':SpecifiedSupplyChainTradeAgreement/' . $ram . ':GrossPriceProductTradePrice/' . $ram . ':ChargeAmount[attribute::currencyID]',
'./' . $ram . ':SpecifiedSupplyChainTradeAgreement/' . $ram . ':GrossPriceProductTradePrice/' . $ram . ':BasisAmount'],
'price' => ['./' . $ram . ':SpecifiedSupplyChainTradeAgreement/' . $ram . ':GrossPriceProductTradePrice/' . $ram . ':ChargeAmount',
'./' . $ram . ':SpecifiedSupplyChainTradeAgreement/' . $ram . ':GrossPriceProductTradePrice/' . $ram . ':BasisAmount'],
'description' => ['./' . $ram . ':SpecifiedTradeProduct/' . $ram . ':Name'],
'quantity' => ['./' . $ram . ':SpecifiedSupplyChainTradeDelivery/' . $ram . ':BilledQuantity',],
'subtotal' => ['./' . $ram . ':SpecifiedSupplyChainTradeSettlement/' . $ram . ':SpecifiedTradeSettlementMonetarySummation/' . $ram . ':LineTotalAmount'],
'tax_rate' => ['./' . $ram . ':SpecifiedSupplyChainTradeSettlement/' . $ram . ':ApplicableTradeTax/' . $ram . ':ApplicablePercent'],
'tax_scheme' => ['./' . $ram . ':SpecifiedSupplyChainTradeSettlement/' . $ram . ':ApplicableTradeTax/' . $ram . ':TypeCode'],
'vendor_partno' => ['./' . $ram . ':SpecifiedTradeProduct/' . $ram . ':SellerAssignedID'],
};
}

sub items_xpath {
my ($self) = @_;
my $rsm = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'};
my $ram = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'};
my $udt = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'};
$ram .= ":" if $ram;
$rsm .= ":" if $rsm;
$udt .= ":" if $udt;
return '//' . $ram . 'IncludedSupplyChainTradeLineItem';
}

# Metadata accessor method
sub metadata {
Expand Down Expand Up @@ -145,6 +180,9 @@ sub parse_xml {
$self->{_metadata} = {};
$self->{_items} = ();

my $ram = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'};
$ram .= ":" if $ram;
my $udt = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'};
# Retrieve scalar metadata from DOM
foreach my $key ( keys %{$self->scalar_xpaths} ) {
foreach my $xpath ( @{${$self->scalar_xpaths}{$key}} ) {
Expand All @@ -154,6 +192,10 @@ sub parse_xml {
next;
}
my $value = $self->{dom}->findnodes($xpath);
unless ($udt) {
$value = $self->{dom}->findnodes('//' . $ram . 'DueDateDateTime','DateTimeString') if $key eq 'duedate';
$value = $self->{dom}->findnodes('//' . $ram . 'IssueDateTime','DateTimeString') if $key eq 'transdate';
}
if ( $value ) {
# Get rid of extraneous white space
$value = $value->string_value;
Expand All @@ -175,7 +217,7 @@ sub parse_xml {
my @items;
$self->{_items} = \@items;

foreach my $item ( $self->{dom}->findnodes(ITEMS_XPATH)) {
foreach my $item ( $self->{dom}->findnodes($self->items_xpath)) {
my %line_item;
foreach my $key ( keys %{$self->item_xpaths} ) {
foreach my $xpath ( @{${$self->item_xpaths}{$key}} ) {
Expand Down
87 changes: 64 additions & 23 deletions SL/XMLInvoice/CrossIndustryInvoice.pm
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use warnings;

use parent qw(SL::XMLInvoice::Base);

use constant ITEMS_XPATH => '//ram:IncludedSupplyChainTradeLineItem';

=head1 NAME
Expand Down Expand Up @@ -51,6 +50,7 @@ returned by the C<items()> method.
=head1 AUTHOR
Johannes Grassler <info@computer-grassler.de>
Werner Hahn <wh@futureworldsearch.net>
=cut

Expand All @@ -73,37 +73,71 @@ sub check_signature {
return 0;
}

sub namespaces {
my ($self, $dom) = @_;
my $rootnode = $dom->documentElement;
my @nodes = $rootnode->findnodes('namespace::*');
my %namespaces = map { $_->getData => $_->getLocalName} @nodes;
return \%namespaces;
}

# XML XPath expressions for global metadata
sub scalar_xpaths {
my ($self) = @_;

my $rsm = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'};
my $ram = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'};
my $udt = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'};
$ram .= ":" if $ram;
$rsm .= ":" if $rsm;
$udt .= ":" if $udt;

return {
currency => '//ram:InvoiceCurrencyCode',
direct_debit => '//ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode',
duedate => '//ram:DueDateDateTime/udt:DateTimeString',
gross_total => '//ram:DuePayableAmount',
iban => '//ram:SpecifiedTradeSettlementPaymentMeans/ram:PayeePartyCreditorFinancialAccount/ram:IBANID',
invnumber => '//rsm:ExchangedDocument/ram:ID',
net_total => '//ram:SpecifiedTradeSettlementHeaderMonetarySummation' . '//ram:TaxBasisTotalAmount',
transdate => '//ram:IssueDateTime/udt:DateTimeString',
taxnumber => '//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]',
type => '//rsm:ExchangedDocument/ram:TypeCode',
ustid => '//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]',
vendor_name => '//ram:SellerTradeParty/ram:Name',
currency => '//' . $ram . 'InvoiceCurrencyCode',
direct_debit => '//' . $ram . 'SpecifiedTradeSettlementPaymentMeans/' . $ram . 'TypeCode',
duedate => '//' . $ram . 'DueDateDateTime/' . $udt . 'DateTimeString',
gross_total => '//' . $ram . 'DuePayableAmount',
iban => '//' . $ram . 'SpecifiedTradeSettlementPaymentMeans/' . $ram . 'PayeePartyCreditorFinancialAccount/' . $ram . 'IBANID',
invnumber => '//' . $rsm . 'ExchangedDocument/' . $ram . 'ID',
net_total => '//' . $ram . 'SpecifiedTradeSettlementHeaderMonetarySummation' . '//' . $ram . 'TaxBasisTotalAmount',
transdate => '//' . $ram . 'IssueDateTime/' . $udt . 'DateTimeString',
taxnumber => '//' . $ram . 'SellerTradeParty/' . $ram . 'SpecifiedTaxRegistration/' . $ram . 'ID[@schemeID="FC"]',
type => '//' . $rsm . 'ExchangedDocument/' . $ram . 'TypeCode',
ustid => '//' . $ram . 'SellerTradeParty/' . $ram . 'SpecifiedTaxRegistration/' . $ram . 'ID[@schemeID="VA"]',
vendor_name => '//' . $ram . 'SellerTradeParty/' . $ram . 'Name',
};
}

sub item_xpaths {
my ($self) = @_;
my $rsm = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'};
my $ram = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'};
my $udt = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'};
$ram .= ":" if $ram;
$rsm .= ":" if $rsm;
$udt .= ":" if $udt;
return {
'currency' => undef, # Only global currency in CrossIndustryInvoice
'price' => './ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice',
'description' => './ram:SpecifiedTradeProduct/ram:Name',
'quantity' => './ram:SpecifiedLineTradeDelivery/ram:BilledQuantity',
'subtotal' => './ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount',
'tax_rate' => './ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent',
'tax_scheme' => './ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:TypeCode',
'vendor_partno' => './ram:SpecifiedTradeProduct/ram:SellerAssignedID',
'currency' => undef, # Only global currency in CrossIndustryInvoice
'price' => './' . $ram . 'SpecifiedLineTradeAgreement/' . $ram . 'NetPriceProductTradePrice',
'description' => './' . $ram . 'SpecifiedTradeProduct/' . $ram . 'Name',
'quantity' => './' . $ram . 'SpecifiedLineTradeDelivery/' . $ram . 'BilledQuantity',
'subtotal' => './' . $ram . 'SpecifiedLineTradeSettlement/' . $ram . 'SpecifiedTradeSettlementLineMonetarySummation/' . $ram . 'LineTotalAmount',
'tax_rate' => './' . $ram . 'SpecifiedLineTradeSettlement/' . $ram . 'ApplicableTradeTax/' . $ram . 'RateApplicablePercent',
'tax_scheme' => './' . $ram . 'SpecifiedLineTradeSettlement/' . $ram . 'ApplicableTradeTax/' . $ram . 'TypeCode',
'vendor_partno' => './' . $ram . 'SpecifiedTradeProduct/' . $ram . 'SellerAssignedID',
};
}

sub items_xpath {
my ($self) = @_;
my $rsm = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'};
my $ram = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'};
my $udt = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'};
$ram .= ":" if $ram;
$rsm .= ":" if $rsm;
$udt .= ":" if $udt;
return '//' . $ram . 'IncludedSupplyChainTradeLineItem';
}

# Metadata accessor method
sub metadata {
Expand Down Expand Up @@ -143,6 +177,10 @@ sub parse_xml {
$self->{_metadata} = {};
$self->{_items} = ();

my $ram = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'};
$ram .= ":" if $ram;
my $udt = $self->{namespaces}->{'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'};
#foreach my $namespace (@{$self->{namespaces}}(
# Retrieve scalar metadata from DOM
foreach my $key ( keys %{$self->scalar_xpaths} ) {
my $xpath = ${$self->scalar_xpaths}{$key};
Expand All @@ -152,6 +190,10 @@ sub parse_xml {
next;
}
my $value = $self->{dom}->findnodes($xpath);
unless ($udt) {
$value = $self->{dom}->findnodes('//' . $ram . 'DueDateDateTime','DateTimeString') if $key eq 'duedate';
$value = $self->{dom}->findnodes('//' . $ram . 'IssueDateTime','DateTimeString') if $key eq 'transdate';
}
if ( $value ) {
# Get rid of extraneous white space
$value = $value->string_value;
Expand All @@ -163,15 +205,14 @@ sub parse_xml {
}
}


# Convert payment code metadata field to Boolean
# See https://service.unece.org/trade/untdid/d16b/tred/tred4461.htm for other valid codes.
${$self->{_metadata}}{'direct_debit'} = ${$self->{_metadata}}{'direct_debit'} == 59 ? 1 : 0;

my @items;
$self->{_items} = \@items;

foreach my $item ( $self->{dom}->findnodes(ITEMS_XPATH) ) {
foreach my $item ( $self->{dom}->findnodes($self->items_xpath) ) {
my %line_item;
foreach my $key ( keys %{$self->item_xpaths} ) {
my $xpath = ${$self->item_xpaths}{$key};
Expand Down

0 comments on commit f9422f0

Please sign in to comment.