diff --git a/SL/XMLInvoice.pm b/SL/XMLInvoice.pm index 5967931479..f43e1c9d8e 100644 --- a/SL/XMLInvoice.pm +++ b/SL/XMLInvoice.pm @@ -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; @@ -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 }; diff --git a/SL/XMLInvoice/Base.pm b/SL/XMLInvoice/Base.pm index 9ba2d7fac0..950e482281 100644 --- a/SL/XMLInvoice/Base.pm +++ b/SL/XMLInvoice/Base.pm @@ -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) @@ -85,6 +86,7 @@ to discover the metadata keys guaranteed to be present. =cut sub item_keys { + my $self = shift; my @keys = ( 'currency', 'description', @@ -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 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 @@ -227,6 +240,7 @@ C. Omitting this method from a child class will cause an exception. =head1 AUTHOR Johannes Grassler + Werner Hahn =cut diff --git a/SL/XMLInvoice/CrossIndustryDocument.pm b/SL/XMLInvoice/CrossIndustryDocument.pm index b5b48d22ff..0775d1dfbb 100644 --- a/SL/XMLInvoice/CrossIndustryDocument.pm +++ b/SL/XMLInvoice/CrossIndustryDocument.pm @@ -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 @@ -51,6 +49,7 @@ returned by the C method. =head1 AUTHOR Johannes Grassler + Werner Hahn =cut @@ -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 { @@ -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}} ) { @@ -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; @@ -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}} ) { diff --git a/SL/XMLInvoice/CrossIndustryInvoice.pm b/SL/XMLInvoice/CrossIndustryInvoice.pm index cf88a9354d..4f8a8e70b8 100644 --- a/SL/XMLInvoice/CrossIndustryInvoice.pm +++ b/SL/XMLInvoice/CrossIndustryInvoice.pm @@ -5,7 +5,6 @@ use warnings; use parent qw(SL::XMLInvoice::Base); -use constant ITEMS_XPATH => '//ram:IncludedSupplyChainTradeLineItem'; =head1 NAME @@ -51,6 +50,7 @@ returned by the C method. =head1 AUTHOR Johannes Grassler + Werner Hahn =cut @@ -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 { @@ -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}; @@ -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; @@ -163,7 +205,6 @@ 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; @@ -171,7 +212,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} ) { my $xpath = ${$self->item_xpaths}{$key};