Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 34e66c3e

Von Johannes Grassler vor mehr als 1 Jahr hinzugefügt

  • ID 34e66c3eea12546e1884ec1f4f437d922f711e10
  • Vorgänger 7f30dd7b
  • Nachfolger d472ce56

SL::XMLInvoice hinzugefuegt

Dieser Commit fuegt das Modul SL::XMLInvoice und seine
Untermodule SL::XMLInvoice::CrossIndustryInvoice und
SL::XMLInvoice::UBL hinzu. Diese Module dienen der
Verarbeitung der folgenden XML-Rechnungsformate:

  • Oasis Universal Business Language 2.1 Invoice (liegt dem
    deutschen XRechnung-Format zugrunde)
  • UN/ECE 4461 Cross Industry Invoice (liegt den ZUGFeRD,
    und Factur-X-Formaten zugrunde)

Diese Module sind reine Helfer, die keine Datenbankoperationen
durchfuehren oder Seiten rendern. Sie verarbeiten XML-Daten
und stellen sie als Perl-Datenstrukturen zur Verfuegung (siehe
POD-Dokumentation der Module).

Unterschiede anzeigen:

SL/XMLInvoice.pm
1
package SL::XMLInvoice;
2

  
3
=head1 NAME
4

  
5
SL::XMLInvoice - Top level factory class for XML Invoice parsers.
6

  
7
=head1 DESCRIPTION
8

  
9
C<SL::XMLInvoice> is an abstraction layer allowing the application to pass any
10
supported XML invoice document for parsing, with C<SL::XMLInvoice> handling all
11
details from there: depending on its document type declaration, this class will
12
pick and instatiate the appropriate child class for parsing the document and
13
return an object exposing its data with the standardized structure outlined
14
below.
15

  
16
=head1 SYNOPSIS
17

  
18
  # $xml_data contains an XML document as flat scalar
19
  my $invoice_parser = SL::XMLInvoice->new($xml_data);
20

  
21
  # %metadata is a hash of document level metadata items
22
  my %metadata = %{$invoice_parser->metadata};
23

  
24
  # @items is an array of hashes, each representing a line
25
  # item on the bill
26
  my @items = @{$invoice_parser->items};
27
                                                        
28
=cut
29

  
30
use strict;
31
use warnings;
32

  
33
use XML::LibXML;
34

  
35
=head1 ATTRIBUTES
36

  
37
=over 4
38

  
39
=item dom
40

  
41
A XML::LibXML document object model (DOM) object created from the XML data supplied.
42

  
43
=item message
44

  
45
Will contain a detailed error message if the C<result> attribute is anything
46
other than C<SL::XMLInvoice::RES_OK>.
47

  
48
=item result
49

  
50
A status field indicating whether the supplied XML data could be parsed. It
51
can take the following values:
52

  
53
=item SL::XMLInvoice::RES_OK
54

  
55
File has been parsed successfully.
56

  
57
=item SL::XMLInvoice::RES_XML_PARSING FAILED
58

  
59
Parsing the file failed.
60

  
61
=item SL::XMLInvoice::RES_UNKNOWN_ROOT_NODE_TYPE
62

  
63
The root node is of an unknown type. Currently, C<rsm:CrossIndustryInvoice> and
64
C<ubl:Invoice> are supported.
65

  
66
=back
67

  
68
=cut
69

  
70
use constant RES_OK                                  => 0;
71
use constant RES_XML_PARSING_FAILED                  => 1;
72
use constant RES_UNKNOWN_ROOT_NODE_TYPE              => 2;
73

  
74
=head1 METHODS
75

  
76
=head2 Data structure definition methods (only in C<SL::XMLInvoice>)
77

  
78
These methods are only implemented in C<SL::XMLInvoice> itself and define the
79
data structures to be exposed by any child classes.
80

  
81
=over 4 
82

  
83
=item data_keys()
84

  
85
Returns all keys the hash returned by any child's C<metadata()> method must
86
contain. If you add keys to this list, you need to add them to all classes
87
inheriting from C<SL::XMLInvoice> as well. An application may use this method
88
to discover the metadata keys guaranteed to be present.
89

  
90
=cut
91

  
92
sub data_keys  {
93
  my @keys = (
94
    'currency',      # The bill's currency, such as "EUR"
95
    'direct_debit',  # Boolean: whether the bill will get paid by direct debit (1) or not (0)
96
    'duedate',       # The bill's due date in YYYY-MM-DD format.
97
    'gross_total',   # The invoice's sum total with tax included
98
    'iban',          # The creditor's IBAN
99
    'invnumber',     # The invoice's number
100
    'net_total',     # The invoice's sum total without tax
101
    'taxnumber',     # The creditor's tax number (Steuernummer in Germany). May be present if
102
                     # there is no VAT ID (USTiD in Germany).
103
    'transdate',     # The date the invoice was issued in YYYY-MM-DD format.
104
    'type',          # Numeric invoice type code, e.g. 380
105
    'ustid',         # The creditor's UStID.
106
    'vendor_name',   # The vendor's company name
107
  );
108
  return \@keys;
109
}
110

  
111
=item item_keys()
112

  
113
Returns all keys the item hashes returned by any child's C<items()> method must
114
contain. If you add keys to this list, you need to add them to all classes
115
inheriting from C<SL::XMLInvoice> as well. An application may use this method
116
to discover the metadata keys guaranteed to be present.
117

  
118
=back
119

  
120
=cut
121

  
122
sub item_keys  {
123
  my @keys = (
124
    'currency',
125
    'description',
126
    'price',
127
    'quantity',
128
    'subtotal',
129
    'tax_rate',
130
    'tax_scheme',
131
    'vendor_partno',
132
  );
133
  return \@keys;
134
}
135

  
136
=head2 User/application facing methods
137

  
138
Any class inheriting from C<SL::XMLInvoice> must implement the following
139
methods. To ensure this happens, C<SL::XMLInvoice> contains stub functions that
140
raise an exception if a child class does not override them.
141

  
142
=over 4
143

  
144
=item new($xml_data)
145

  
146
Constructor for C<SL::XMLInvoice>. This method takes a scalar containing the
147
entire XML document to be parsed as a flat string as its sole argument. It will
148
instantiate the appropriate child class to parse the XML document in question,
149
call its C<parse_xml> method and return the C<SL::XMLInvoice> child object it
150
instantiated. From that point on, the structured data retrieved from the XML
151
document will be available through the object's C<metadata> and C<items()>
152
methods.
153

  
154
=item metadata()
155

  
156
This method returns a hash of document level metadata, such as the invoice
157
number, the total, or the the issuance date. Its keys are the keys returned by
158
the C<(data_keys()> method. Its values are plain scalars containing strings or
159
C<undef> for any data items not present or empty in the XML document.
160

  
161
=cut
162

  
163
sub metadata {
164
  my $self = shift;
165
  die "Children of $self must implement a metadata() method returning the bill's metadata as a hash.";
166
  }
167

  
168
=item items()
169

  
170
This method returns an array of hashes containing line item metadata, such as
171
the quantity, price for one unit, or subtotal. These hashes' keys are the keys
172
returned by the C<(item_keys()> method. Its values are plain scalars containing
173
strings or C<undef> for any data items not present or empty in the XML
174
document.
175

  
176
=cut
177

  
178
sub items {
179
  my $self = shift;
180
  die "Children of $self must implement a item() method returning the bill's items as a hash.";
181
  }
182

  
183
=item parse_xml()
184

  
185
This method is only implemented in child classes of C<SL::XMLInvoice> and is
186
called by the C<SL::XMLInvoice> constructor once the appropriate child class has been
187
determined and instantiated. It uses C<$self->{dom}>, an C<XML::LibXML>
188
instance to iterate through the XML document to parse. That XML document is
189
created by the C<SL::XMLInvoice> constructor.
190

  
191
=back
192

  
193
=cut
194

  
195
sub parse_xml {
196
  my $self = shift;
197
  die "Children of $self must implement a parse_xml() method.";
198
  }
199

  
200
=head2 Internal methods
201

  
202
These methods' purpose is child classs selection and making sure child classes
203
implent the interface promised by C<SL::XMLInvoice>. You can safely ignore them
204
if you don't plan on implementing any child classes.
205

  
206

  
207
=item _document_nodenames()
208

  
209
This method is implemented in C<SL::XMLInvoice> only and returns a hash mapping
210
XML document root node name to a child class implementing a parser for it. If
211
you add any child classes for new XML document types you need to add them to
212
this hash and add a use statement to make it available from C<SL::XMLInvoice>.
213

  
214
=cut
215

  
216
use SL::XMLInvoice::UBL;
217
use SL::XMLInvoice::CrossIndustryInvoice;
218

  
219
sub _document_nodenames {
220
  return { 
221
    'rsm:CrossIndustryInvoice' => 'SL::XMLInvoice::CrossIndustryInvoice',
222
    'ubl:Invoice' => 'SL::XMLInvoice::UBL',
223
  };
224
}
225

  
226
=item _data_keys()
227

  
228
Returns a list of all keys present in the hash returned by the class'
229
C<metadata()> method. Must be implemented in all classes inheriting from
230
C<SL::XMLInvoice> This list must contain the same keys as the list returned by
231
C<data_keys>. Omitting this method from a child class will cause an exception.
232

  
233
=cut
234

  
235
sub _data_keys {
236
  my $self = shift;
237
  die "Children of $self must implement a _data_keys() method returning the keys an invoice item hash will contain.";
238
  }
239

  
240
=item _item_keys()
241

  
242
Returns a list of all keys present in the hashes returned by the class'
243
C<items()> method. Must be implemented in all classes inheriting from
244
C<SL::XMLInvoice> This list must contain the same keys as the list returned by
245
C<item_keys>. Omitting this method from a child class will cause an exception.
246

  
247
=head1 AUTHOR
248

  
249
  Johannes Grassler <info@computer-grassler.de>
250

  
251
=cut
252

  
253
sub _item_keys {
254
  my $self = shift;
255
  die "Children of $self must implement a _item_keys() method returning the keys an invoice item hash will contain.";
256
  }
257

  
258

  
259
sub new
260
  {
261
  my ($self, $xml_data) = @_;
262
  my $type = undef;
263
  $self = {};
264

  
265
  bless $self;
266

  
267
  $self->{message} = '';
268
  $self->{dom} = eval { XML::LibXML->load_xml(string => $xml_data) };
269

  
270
  if ( ! $self->{dom} ) {
271
    $self->{message} = $::locale->text("Parsing the XML data failed: $xml_data");
272
    $self->{result} = RES_XML_PARSING_FAILED;
273
    return $self;
274
    }
275

  
276
  # Determine parser class to use
277
  my $document_nodename = $self->{dom}->documentElement->nodeName;
278
  if ( ${$self->_document_nodenames}{$document_nodename} ) {
279
    $type = ${$self->_document_nodenames}{$document_nodename}
280
    }
281

  
282
  unless ( $type ) {
283
    $self->{result} = RES_UNKNOWN_ROOT_NODE_TYPE;
284
    my $node_types = keys %{ $self->_document_nodenames };
285
    $self->{message} =  t8("Could not parse XML Invoice: unknown root node name (#1) (supported: (#2))",
286
                           $node_types,
287
                           $document_nodename);
288
    return $self;
289
    }
290

  
291
  bless $self, $type;
292

  
293
  # Implementation sanity check for child classes: make sure they are aware of
294
  # the keys the hash returned by their metadata() method must contain.
295
  my @missing_data_keys = ();
296
  foreach my $data_key ( @{$self->data_keys} )
297
    {
298
    unless ( ${$self->_data_keys}{$data_key}) { push @missing_data_keys, $data_key; }
299
    }
300
  if ( scalar(@missing_data_keys) > 0 ) {
301
    die "Incomplete implementation: the following metadata keys appear to be missing from $type: " . join(", ", @missing_data_keys);
302
  }
303

  
304
  # Implementation sanity check for child classes: make sure they are aware of
305
  # the keys the hashes returned by their items() method must contain.
306
  my @missing_item_keys = ();
307
  foreach my $item_key ( @{$self->item_keys} )
308
    {
309
    unless ( ${$self->_item_keys}{$item_key}) { push @missing_item_keys, $item_key; }
310
    }
311
  if ( scalar(@missing_item_keys) > 0 ) {
312
    die "Incomplete implementation: the following item keys appear to be missing from $type: " . join(", ", @missing_item_keys);
313
  }
314

  
315
  $self->parse_xml;
316

  
317
  # Ensure these methods are implemented in the child class
318
  $self->metadata;
319
  $self->items;
320

  
321
  $self->{result} = RES_OK;
322
  return $self;
323
  }
324

  
325
1;
326

  
SL/XMLInvoice/CrossIndustryInvoice.pm
1
package SL::XMLInvoice::CrossIndustryInvoice;
2
use parent qw(SL::XMLInvoice);
3

  
4
=head1 NAME
5

  
6
SL::XMLInvoice::FakturX - XML parser for UN/CEFACT Cross Industry Invoice
7

  
8
=head1 DESCRIPTION
9

  
10
C<SL::XMLInvoice::CrossIndustryInvoice> parses XML invoices in UN/CEFACT Cross
11
Industry Invoice format and makes their data available through the interface
12
defined by C<SL::XMLInvoice>. Refer to L<SL::XMLInvoice> for a detailed
13
description of that interface.
14

  
15
See L<https://unece.org/trade/uncefact/xml-schemas> for that format's
16
specification.
17

  
18
=head1 OPERATION
19

  
20
This module is fairly simple. It keeps two hashes of XPath statements exposed
21
by methods:
22

  
23
=over 4
24

  
25
=item scalar_xpaths()
26

  
27
This hash is keyed by the keywords C<data_keys> mandates. Values are XPath
28
statements specifying the location of this field in the invoice XML document.
29

  
30
=item item_xpaths()
31

  
32
This hash is keyed by the keywords C<item_keys> mandates. Values are XPath
33
statements specifying the location of this field inside a line item.
34

  
35
=back
36

  
37
When invoked by the C<SL::XMLInvoice> constructor, C<parse_xml()> will first
38
use the XPath statements from the C<scalar_xpaths()> hash to populate the hash
39
returned by the C<metadata()> method.
40

  
41
After that, it will use the XPath statements from the C<scalar_xpaths()> hash
42
to iterate over the invoice's line items and populate the array of hashes
43
returned by the C<items()> method.
44

  
45
=head1 AUTHOR
46

  
47
  Johannes Grassler <info@computer-grassler.de>
48

  
49
=cut
50

  
51
use strict;
52
use constant ITEMS_XPATH => '//ram:IncludedSupplyChainTradeLineItem';
53

  
54
# XML XPath expressions for global metadata
55
sub scalar_xpaths {
56
  return {
57
    currency => '//ram:InvoiceCurrencyCode',
58
    direct_debit => '//ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode',
59
    duedate => '//ram:DueDateDateTime/udt:DateTimeString',
60
    gross_total => '//ram:DuePayableAmount',
61
    iban => '//ram:SpecifiedTradeSettlementPaymentMeans/ram:PayeePartyCreditorFinancialAccount/ram:IBANID',
62
    invnumber => '//rsm:ExchangedDocument/ram:ID',
63
    net_total => '//ram:SpecifiedTradeSettlementHeaderMonetarySummation' . '//ram:TaxBasisTotalAmount',
64
    transdate => '//ram:IssueDateTime/udt:DateTimeString',
65
    taxnumber => '//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]',
66
    type => '//rsm:ExchangedDocument/ram:TypeCode',
67
    ustid => '//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]',
68
    vendor_name => '//ram:SellerTradeParty/ram:Name',
69
  };
70
}
71

  
72
sub item_xpaths {
73
  return {
74
    'currency' => undef, # Only global currency in CrossIndustryInvoice
75
    'price' => './ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice',
76
    'description' => './ram:SpecifiedTradeProduct/ram:Name',
77
    'quantity' => './ram:SpecifiedLineTradeDelivery/ram:BilledQuantity',
78
    'subtotal' => './ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount',
79
    'tax_rate' => './ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent',
80
    'tax_scheme' => './ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:TypeCode',
81
    'vendor_partno' => './ram:SpecifiedTradeProduct/ram:SellerAssignedID',
82
  };
83
}
84

  
85

  
86
# Metadata accessor method
87
sub metadata {
88
  my $self = shift;
89
  return $self->{_metadata};
90
}
91

  
92
# Item list accessor method
93
sub items {
94
  my $self = shift;
95
  return $self->{_items};
96
}
97

  
98
# Data keys we return
99
sub _data_keys {
100
  my $self = shift;
101
  my %keys;
102

  
103
  map { $keys{$_} = 1; } keys %{$self->scalar_xpaths};
104

  
105
  return \%keys;
106
}
107

  
108
# Item keys we return
109
sub _item_keys {
110
  my $self = shift;
111
  my %keys;
112

  
113
  map { $keys{$_} = 1; } keys %{$self->item_xpaths};
114

  
115
  return \%keys;
116
}
117

  
118
# Main parser subroutine for retrieving XML data
119
sub parse_xml {
120
  my $self = shift;
121
  $self->{_metadata} = {};
122
  $self->{_items} = ();
123

  
124
  # Retrieve scalar metadata from DOM
125
  foreach my $key ( keys %{$self->scalar_xpaths} ) {
126
    my $xpath = ${$self->scalar_xpaths}{$key};
127
    unless ( $xpath ) {
128
      # Skip keys without xpath expression
129
      ${$self->{_metadata}}{$key} = undef;
130
      next;
131
    }
132
    my $value = $self->{dom}->findnodes($xpath);
133
    if ( $value ) {
134
      # Get rid of extraneous white space
135
      $value = $value->string_value;
136
      $value =~ s/\n|\r//g;
137
      $value =~ s/\s{2,}/ /g;
138
      ${$self->{_metadata}}{$key} = $value;
139
    } else {
140
      ${$self->{_metadata}}{$key} = undef;
141
    }
142
  }
143

  
144

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

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

  
152
  foreach my $item ( $self->{dom}->findnodes(ITEMS_XPATH) ) {
153
    my %line_item;
154
    foreach my $key ( keys %{$self->item_xpaths} ) {
155
      my $xpath = ${$self->item_xpaths}{$key};
156
      unless ( $xpath ) {
157
        # Skip keys without xpath expression
158
        $line_item{$key} = undef;
159
        next;
160
      }
161
      my $value = $item->findnodes($xpath);
162
      if ( $value ) {
163
        # Get rid of extraneous white space
164
        $value = $value->string_value;
165
        $value =~ s/\n|\r//g;
166
        $value =~ s/\s{2,}/ /g;
167
        $line_item{$key} = $value;
168
      } else {
169
        $line_item{$key} = undef;
170
      }
171
    }
172
    push @items, \%line_item;
173
  }
174

  
175

  
176
}
177

  
178
1;
SL/XMLInvoice/UBL.pm
1
package SL::XMLInvoice::UBL;
2

  
3
=head1 NAME
4

  
5
SL::XMLInvoice::UBL - XML parser for Universal Business Language invoices
6

  
7
=head1 DESCRIPTION
8

  
9
C<SL::XMLInvoice::UBL> parses XML invoices in Oasis Universal Business
10
Language format and makes their data available through the interface defined
11
by C<SL::XMLInvoice>. Refer to L<SL::XMLInvoice> for a detailed description of
12
that interface.
13

  
14
See L<http://docs.oasis-open.org/ubl/os-UBL-2.1/UBL-2.1.html#T-INVOICE> for
15
that format's specification.
16

  
17
=head1 OPERATION
18

  
19
This module is fairly simple. It keeps two hashes of XPath statements exposed
20
by methods:
21

  
22
=over 4
23

  
24
=item scalar_xpaths()
25

  
26
This hash is keyed by the keywords C<data_keys> mandates. Values are XPath
27
statements specifying the location of this field in the invoice XML document.
28

  
29
=item item_xpaths()
30

  
31
This hash is keyed by the keywords C<item_keys> mandates. Values are XPath
32
statements specifying the location of this field inside a line item.
33

  
34
=back
35

  
36
When invoked by the C<SL::XMLInvoice> constructor, C<parse_xml()> will first
37
use the XPath statements from the C<scalar_xpaths()> hash to populate the hash
38
returned by the C<metadata()> method.
39

  
40
After that, it will use the XPath statements from the C<scalar_xpaths()> hash
41
to iterate over the invoice's line items and populate the array of hashes
42
returned by the C<items()> method.
43

  
44
=head1 AUTHOR
45

  
46
  Johannes Grassler <info@computer-grassler.de>
47

  
48
=cut
49

  
50
use strict;
51
use parent qw(SL::XMLInvoice);
52

  
53
use constant ITEMS_XPATH => '//cac:InvoiceLine';
54

  
55
# XML XPath expression for
56
sub scalar_xpaths {
57
  return {
58
    currency => '//cbc:DocumentCurrencyCode',
59
    direct_debit => '//cbc:PaymentMeansCode[@listID="UN/ECE 4461"]',
60
    duedate => '//cbc:DueDate',
61
    gross_total => '//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount',
62
    iban => '//cac:PayeeFinancialAccount/cbc:ID',
63
    invnumber => '//cbc:ID',
64
    net_total => '//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount',
65
    transdate => '//cbc:IssueDate',
66
    type => '//cbc:InvoiceTypeCode',
67
    taxnumber => '//cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
68
    ustid => '//cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
69
    vendor_name => '//cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name',
70
  };
71
}
72

  
73
sub item_xpaths {
74
  return {
75
    'currency' => './cbc:LineExtensionAmount[attribute::currencyID]',
76
    'price' => './cac:Price/cbc:PriceAmount',
77
    'description' => './cac:Item/cbc:Description',
78
    'quantity' => './cbc:InvoicedQuantity',
79
    'subtotal' => './cbc:LineExtensionAmount',
80
    'tax_rate' => './/cac:ClassifiedTaxCategory/cbc:Percent',
81
    'tax_scheme' => './cac:Item/cac:ClassifiedTaxCategory/cac:TaxScheme/cbc:ID',
82
    'vendor_partno' => './cac:Item/cac:SellersItemIdentification/cbc:ID',
83
  };
84
}
85

  
86

  
87
# Metadata accessor method
88
sub metadata {
89
  my $self = shift;
90
  return $self->{_metadata};
91
}
92

  
93
# Item list accessor method
94
sub items {
95
  my $self = shift;
96
  return $self->{_items};
97
}
98

  
99
# Data keys we return
100
sub _data_keys {
101
  my $self = shift;
102
  my %keys;
103

  
104
  map { $keys{$_} = 1; } keys %{$self->scalar_xpaths};
105

  
106
  return \%keys;
107
}
108

  
109
# Item keys we return
110
sub _item_keys {
111
  my $self = shift;
112
  my %keys;
113

  
114
  map { $keys{$_} = 1; } keys %{$self->item_xpaths};
115

  
116
  return \%keys;
117
}
118

  
119
# Main parser subroutine for retrieving XML data
120
sub parse_xml {
121
  my $self = shift;
122
  $self->{_metadata} = {};
123
  $self->{_items} = ();
124

  
125
  # Retrieve scalar metadata from DOM
126
  foreach my $key ( keys %{$self->scalar_xpaths} ) {
127
    my $xpath = ${$self->scalar_xpaths}{$key};
128
    my $value = $self->{dom}->findnodes($xpath);
129
    if ( $value ) {
130
      # Get rid of extraneous white space
131
      $value = $value->string_value;
132
      $value =~ s/\n|\r//g;
133
      $value =~ s/\s{2,}/ /g;
134
      ${$self->{_metadata}}{$key} = $value;
135
    } else {
136
      ${$self->{_metadata}}{$key} = undef;
137
    }
138
  }
139

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

  
144
  # UBL does not have a specified way of designating the tax scheme, so we'll
145
  # have to guess whether it's a tax ID or VAT ID (not using
146
  # SL::VATIDNr->validate here to keep this code portable):
147

  
148
  if ( ${$self->{_metadata}}{'ustid'} =~ qr"/" )
149
    {
150
      # Unset this since the 'taxid' key has been retrieved with the same xpath
151
      # expression.
152
      ${$self->{_metadata}}{'ustid'} = undef;
153
    } else {
154
      # Unset this since the 'ustid' key has been retrieved with the same xpath
155
      # expression.
156
      ${$self->{_metadata}}{'taxnumber'} = undef;
157
    }
158

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

  
162
  foreach my $item ( $self->{dom}->findnodes(ITEMS_XPATH) ) {
163
    my %line_item;
164
    foreach my $key ( keys %{$self->item_xpaths} ) {
165
      my $xpath = ${$self->item_xpaths}{$key};
166
      my $value = $item->findnodes($xpath);
167
      if ( $value ) {
168
        # Get rid of extraneous white space
169
        $value = $value->string_value;
170
        $value =~ s/\n|\r//g;
171
        $value =~ s/\s{2,}/ /g;
172
        $line_item{$key} = $value;
173
      } else {
174
        $line_item{$key} = undef;
175
      }
176
    }
177
    push @items, \%line_item;
178
  }
179

  
180

  
181
}
182

  
183
1;

Auch abrufbar als: Unified diff