Revision 335b5ab6
Von Johannes Grassler vor etwa 1 Jahr hinzugefügt
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
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:
deutschen XRechnung-Format zugrunde)
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).