Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XMLInvoice: gültige namespaces aus der xml holen ... #419

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Das hier finde ich unschön.

Das ist ein bisschen akademisch, aber die Struktur hier ist ein Factory Pattern, wo die SL::XMLInvoice für eine Datei den passenden Worker instanziert. Das Instanzieren passiert hier aber nicht über einen Constructor, sondern in dem das hashref $self einfach in das entsprechende package ge-bless-ed wird.

Nach dem bless gilt das Worker-Objekt als fertig, und ab da sollte auch nicht mehr seinen Innereien gewühlt werden, sondern stattdessen mit den Objektmethoden drauf zugegriffen werden.

Möglichkeiten das anders zu machen wären also:

  1. Du machst das vor dem bless (also einfach 2 Zeilen hochschieben).
  2. Du änderst die sub namespaces so, dass sie nicht mehr statisch ist (was sie eh nicht ist, weil sie ja zum Aufrufzeitpunkt schon ein Objekt hat), und lässt sie die namespaces selber in $self->{namespaces} speichern. Dann ist in der SL::XMLInvoice der Aufruf nur noch $self->namespaces($self->{dom}) oder sogar nur $self->namespaces - weil es nicht mehr statisch ist.

In beiden Fällen aber als Idee:

  • Hashzugriffe ($self->{...}) nur vor bless $self, $type
  • Danach nur Methodenzugriffe ($self->method())

# 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 <[email protected]>
Werner Hahn <[email protected]>

=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 <[email protected]>
Werner Hahn <[email protected]>

=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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das hier sieht falsch aus. Müsste auch ein hash zurückgegeben werden wie in der anderen Datei oder?

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'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mein Beileid, dass Du den Kram so tippen musstest. Ich habe geschaut ob das irgendwie eleganter geht, habe aber auch nichts gefunden. LibXML unterstützt leider kein XPath 3.0, da hätte es eine Syntax zumindest für namespaces per url gegeben. Optionale namespaces sind aber anscheinend überhaupt nicht unterstützt.

Ich habe aber auch nochmal im Zugferd Standard nachgeschaut, und wenn ich nicht blind bin, sind die namespaces nicht optional. Für mich wäre das also hier auch absolut okay, wenn das konsequent einen Fehler wirft wenn die 3 namespaces nicht gefunden werden.

Ist aber kein Grund das nochmal anzufassen.

};
}

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';
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das braucht nur den ram namespace - braucht die anderen beiden hier also nicht reinkopieren.

Wenn Du das so oft kopierst, macht evtl eine Methode Sinn, die den namespace nachschlägt und wenn vorhanden mit : Suffix zurückgibt, oder?


# 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 <[email protected]>
Werner Hahn <[email protected]>

=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';
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diesen Block hier verstehe ich nicht.

Das ist in der Schleife für alle scalaren xpaths, und macht dann eine Ausnahme für duedate und transdate wenn der udt namespace nicht gefunden wurde und sucht dann durch das ganze DOM nach den nodes? Gibt es da einen Präzedenzfall? Ist das ein bekannter Bug in einer Datei gewesen?

Bitte zumindest dokumentieren was das sein soll.

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