|
1 |
package SL::XMLInvoice::CrossIndustryDocument;
|
|
2 |
|
|
3 |
use strict;
|
|
4 |
use warnings;
|
|
5 |
|
|
6 |
use parent qw(SL::XMLInvoice);
|
|
7 |
|
|
8 |
use constant ITEMS_XPATH => '//ram:IncludedSupplyChainTradeLineItem';
|
|
9 |
|
|
10 |
=head1 NAME
|
|
11 |
|
|
12 |
SL::XMLInvoice::CrossIndustryDocument - XML parser for UN/CEFACT Cross Industry Document
|
|
13 |
|
|
14 |
=head1 DESCRIPTION
|
|
15 |
|
|
16 |
C<SL::XMLInvoice::CrossIndustryInvoice> parses XML invoices in UN/CEFACT Cross
|
|
17 |
Industry Document format (also known as ZUgFeRD 1p0 or ZUgFeRD 1.0) and makes
|
|
18 |
their data available through the interface defined by C<SL::XMLInvoice>. Refer
|
|
19 |
to L<SL::XMLInvoice> for a detailed description of that interface.
|
|
20 |
|
|
21 |
See L<https://unece.org/trade/uncefact/xml-schemas> for that format's
|
|
22 |
specification.
|
|
23 |
|
|
24 |
=head1 OPERATION
|
|
25 |
|
|
26 |
This module is fairly simple. It keeps two hashes of XPath statements exposed
|
|
27 |
by methods:
|
|
28 |
|
|
29 |
=over 4
|
|
30 |
|
|
31 |
=item scalar_xpaths()
|
|
32 |
|
|
33 |
This hash is keyed by the keywords C<data_keys> mandates. Values are XPath
|
|
34 |
statements specifying the location of this field in the invoice XML document.
|
|
35 |
|
|
36 |
=item item_xpaths()
|
|
37 |
|
|
38 |
This hash is keyed by the keywords C<item_keys> mandates. Values are XPath
|
|
39 |
statements specifying the location of this field inside a line item.
|
|
40 |
|
|
41 |
=back
|
|
42 |
|
|
43 |
When invoked by the C<SL::XMLInvoice> constructor, C<parse_xml()> will first
|
|
44 |
use the XPath statements from the C<scalar_xpaths()> hash to populate the hash
|
|
45 |
returned by the C<metadata()> method.
|
|
46 |
|
|
47 |
After that, it will use the XPath statements from the C<scalar_xpaths()> hash
|
|
48 |
to iterate over the invoice's line items and populate the array of hashes
|
|
49 |
returned by the C<items()> method.
|
|
50 |
|
|
51 |
=head1 AUTHOR
|
|
52 |
|
|
53 |
Johannes Grassler <info@computer-grassler.de>
|
|
54 |
|
|
55 |
=cut
|
|
56 |
|
|
57 |
sub supported {
|
|
58 |
my @supported = ( "UN/CEFACT Cross Industry Document/ZUGFeRD 1.0 (urn:ferd:CrossIndustryDocument:invoice:1p0)" );
|
|
59 |
return @supported;
|
|
60 |
}
|
|
61 |
|
|
62 |
sub check_signature {
|
|
63 |
my ($self, $dom) = @_;
|
|
64 |
|
|
65 |
my $rootnode = $dom->documentElement;
|
|
66 |
|
|
67 |
foreach my $attr ( $rootnode->attributes ) {
|
|
68 |
if ( $attr->getData =~ m/urn:ferd:CrossIndustryDocument:invoice:1p0/ ) {
|
|
69 |
return 1;
|
|
70 |
}
|
|
71 |
}
|
|
72 |
|
|
73 |
return 0;
|
|
74 |
}
|
|
75 |
|
|
76 |
# XML XPath expressions for global metadata
|
|
77 |
sub scalar_xpaths {
|
|
78 |
return {
|
|
79 |
currency => ['//ram:InvoiceCurrencyCode'],
|
|
80 |
direct_debit => ['//ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode'],
|
|
81 |
duedate => ['//ram:DueDateDateTime/udt:DateTimeString', '//ram:EffectiveSpecifiedPeriod/ram:CompleteDateTime/udt:DateTimeString'],
|
|
82 |
gross_total => ['//ram:DuePayableAmount'],
|
|
83 |
iban => ['//ram:SpecifiedTradeSettlementPaymentMeans/ram:PayeePartyCreditorFinancialAccount/ram:IBANID'],
|
|
84 |
invnumber => ['//rsm:HeaderExchangedDocument/ram:ID'],
|
|
85 |
net_total => ['//ram:TaxBasisTotalAmount'],
|
|
86 |
transdate => ['//ram:IssueDateTime/udt:DateTimeString'],
|
|
87 |
taxnumber => ['//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]'],
|
|
88 |
type => ['//rsm:HeaderExchangedDocument/ram:TypeCode'],
|
|
89 |
ustid => ['//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="VA"]'],
|
|
90 |
vendor_name => ['//ram:SellerTradeParty/ram:Name'],
|
|
91 |
};
|
|
92 |
}
|
|
93 |
|
|
94 |
sub item_xpaths {
|
|
95 |
return {
|
|
96 |
'currency' => ['./ram:SpecifiedSupplyChainTradeAgreement/ram:GrossPriceProductTradePrice/ram:ChargeAmount[attribute::currencyID]',
|
|
97 |
'./ram:SpecifiedSupplyChainTradeAgreement/ram:GrossPriceProductTradePrice/ram:BasisAmount'],
|
|
98 |
'price' => ['./ram:SpecifiedSupplyChainTradeAgreement/ram:GrossPriceProductTradePrice/ram:ChargeAmount',
|
|
99 |
'./ram:SpecifiedSupplyChainTradeAgreement/ram:GrossPriceProductTradePrice/ram:BasisAmount'],
|
|
100 |
'description' => ['./ram:SpecifiedTradeProduct/ram:Name'],
|
|
101 |
'quantity' => ['./ram:SpecifiedSupplyChainTradeDelivery/ram:BilledQuantity',],
|
|
102 |
'subtotal' => ['./ram:SpecifiedSupplyChainTradeSettlement/ram:SpecifiedTradeSettlementMonetarySummation/ram:LineTotalAmount'],
|
|
103 |
'tax_rate' => ['./ram:SpecifiedSupplyChainTradeSettlement/ram:ApplicableTradeTax/ram:ApplicablePercent'],
|
|
104 |
'tax_scheme' => ['./ram:SpecifiedSupplyChainTradeSettlement/ram:ApplicableTradeTax/ram:TypeCode'],
|
|
105 |
'vendor_partno' => ['./ram:SpecifiedTradeProduct/ram:SellerAssignedID'],
|
|
106 |
};
|
|
107 |
}
|
|
108 |
|
|
109 |
|
|
110 |
# Metadata accessor method
|
|
111 |
sub metadata {
|
|
112 |
my $self = shift;
|
|
113 |
return $self->{_metadata};
|
|
114 |
}
|
|
115 |
|
|
116 |
# Item list accessor method
|
|
117 |
sub items {
|
|
118 |
my $self = shift;
|
|
119 |
return $self->{_items};
|
|
120 |
}
|
|
121 |
|
|
122 |
# Data keys we return
|
|
123 |
sub _data_keys {
|
|
124 |
my $self = shift;
|
|
125 |
my %keys;
|
|
126 |
|
|
127 |
map { $keys{$_} = 1; } keys %{$self->scalar_xpaths};
|
|
128 |
|
|
129 |
return \%keys;
|
|
130 |
}
|
|
131 |
|
|
132 |
# Item keys we return
|
|
133 |
sub _item_keys {
|
|
134 |
my $self = shift;
|
|
135 |
my %keys;
|
|
136 |
|
|
137 |
map { $keys{$_} = 1; } keys %{$self->item_xpaths};
|
|
138 |
|
|
139 |
return \%keys;
|
|
140 |
}
|
|
141 |
|
|
142 |
# Main parser subroutine for retrieving XML data
|
|
143 |
sub parse_xml {
|
|
144 |
my $self = shift;
|
|
145 |
$self->{_metadata} = {};
|
|
146 |
$self->{_items} = ();
|
|
147 |
|
|
148 |
# Retrieve scalar metadata from DOM
|
|
149 |
foreach my $key ( keys %{$self->scalar_xpaths} ) {
|
|
150 |
foreach my $xpath ( @{${$self->scalar_xpaths}{$key}} ) {
|
|
151 |
unless ( $xpath ) {
|
|
152 |
# Skip keys without xpath list
|
|
153 |
${$self->{_metadata}}{$key} = undef;
|
|
154 |
next;
|
|
155 |
}
|
|
156 |
my $value = $self->{dom}->findnodes($xpath);
|
|
157 |
if ( $value ) {
|
|
158 |
# Get rid of extraneous white space
|
|
159 |
$value = $value->string_value;
|
|
160 |
$value =~ s/\n|\r//g;
|
|
161 |
$value =~ s/\s{2,}/ /g;
|
|
162 |
${$self->{_metadata}}{$key} = $value;
|
|
163 |
last; # first matching xpath wins
|
|
164 |
} else {
|
|
165 |
${$self->{_metadata}}{$key} = undef;
|
|
166 |
}
|
|
167 |
}
|
|
168 |
}
|
|
169 |
|
|
170 |
|
|
171 |
# Convert payment code metadata field to Boolean
|
|
172 |
# See https://service.unece.org/trade/untdid/d16b/tred/tred4461.htm for other valid codes.
|
|
173 |
${$self->{_metadata}}{'direct_debit'} = ${$self->{_metadata}}{'direct_debit'} == 59 ? 1 : 0;
|
|
174 |
|
|
175 |
my @items;
|
|
176 |
$self->{_items} = \@items;
|
|
177 |
|
|
178 |
foreach my $item ( $self->{dom}->findnodes(ITEMS_XPATH)) {
|
|
179 |
my %line_item;
|
|
180 |
foreach my $key ( keys %{$self->item_xpaths} ) {
|
|
181 |
foreach my $xpath ( @{${$self->item_xpaths}{$key}} ) {
|
|
182 |
unless ( $xpath ) {
|
|
183 |
# Skip keys without xpath list
|
|
184 |
$line_item{$key} = undef;
|
|
185 |
next;
|
|
186 |
}
|
|
187 |
my $value = $item->findnodes($xpath);
|
|
188 |
if ( $value ) {
|
|
189 |
# Get rid of extraneous white space
|
|
190 |
$value = $value->string_value;
|
|
191 |
$value =~ s/\n|\r//g;
|
|
192 |
$value =~ s/\s{2,}/ /g;
|
|
193 |
$line_item{$key} = $value;
|
|
194 |
last; # first matching xpath wins
|
|
195 |
} else {
|
|
196 |
$line_item{$key} = undef;
|
|
197 |
}
|
|
198 |
}
|
|
199 |
}
|
|
200 |
push @items, \%line_item;
|
|
201 |
}
|
|
202 |
|
|
203 |
}
|
|
204 |
|
|
205 |
1;
|
SL::XMLInvoice::CrossindustryDocument hinzugefuegt
Dieses Modul ist der Parser fuer Eingangsrechnungen im Format
ZUGFeRD 1.0 / CrossIndustryDocument.