]> wagnertech.de Git - mfinanz.git/blob - SL/XMLInvoice/CrossIndustryInvoice.pm
date error in mapping
[mfinanz.git] / SL / XMLInvoice / CrossIndustryInvoice.pm
1 package SL::XMLInvoice::CrossIndustryInvoice;
2
3 use strict;
4 use warnings;
5
6 use parent qw(SL::XMLInvoice::Base);
7
8 use constant ITEMS_XPATH => '//ram:IncludedSupplyChainTradeLineItem';
9
10 =head1 NAME
11
12 SL::XMLInvoice::CrossIndustryInvoice - XML parser for UN/CEFACT Cross Industry Invoice
13
14 =head1 DESCRIPTION
15
16 C<SL::XMLInvoice::CrossIndustryInvoice> parses XML invoices in UN/CEFACT Cross
17 Industry Invoice format and makes their data available through the interface
18 defined by C<SL::XMLInvoice>. Refer to L<SL::XMLInvoice> for a detailed
19 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 Invoice (urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100)" );
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:un:unece:uncefact:data:standard:CrossIndustryInvoice:100/ ) {
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',
82     gross_total => '//ram:DuePayableAmount',
83     iban => '//ram:SpecifiedTradeSettlementPaymentMeans/ram:PayeePartyCreditorFinancialAccount/ram:IBANID',
84     invnumber => '//rsm:ExchangedDocument/ram:ID',
85     net_total => '//ram:SpecifiedTradeSettlementHeaderMonetarySummation' . '//ram:TaxBasisTotalAmount',
86     transdate => '//ram:IssueDateTime/udt:DateTimeString',
87     taxnumber => '//ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID[@schemeID="FC"]',
88     type => '//rsm:ExchangedDocument/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' => undef, # Only global currency in CrossIndustryInvoice
97     'price' => './ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice',
98     'description' => './ram:SpecifiedTradeProduct/ram:Name',
99     'quantity' => './ram:SpecifiedLineTradeDelivery/ram:BilledQuantity',
100     'subtotal' => './ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount',
101     'tax_rate' => './ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent',
102     'tax_scheme' => './ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:TypeCode',
103     'vendor_partno' => './ram:SpecifiedTradeProduct/ram:SellerAssignedID',
104   };
105 }
106
107
108 # Metadata accessor method
109 sub metadata {
110   my $self = shift;
111   return $self->{_metadata};
112 }
113
114 # Item list accessor method
115 sub items {
116   my $self = shift;
117   return $self->{_items};
118 }
119
120 sub _xpath_context {
121   my $xc = XML::LibXML::XPathContext->new;
122   $xc->registerNs(udt => 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100');
123   $xc->registerNs(ram => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100');
124   $xc->registerNs(rsm => 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100');
125   $xc;
126 }
127
128 # Data keys we return
129 sub _data_keys {
130   my $self = shift;
131   my %keys;
132
133   map { $keys{$_} = 1; } keys %{$self->scalar_xpaths};
134
135   return \%keys;
136 }
137
138 # Item keys we return
139 sub _item_keys {
140   my $self = shift;
141   my %keys;
142
143   map { $keys{$_} = 1; } keys %{$self->item_xpaths};
144
145   return \%keys;
146 }
147
148 # Main parser subroutine for retrieving XML data
149 sub parse_xml {
150   my $self = shift;
151   $self->{_metadata} = {};
152   $self->{_items} = ();
153
154   my $xc = _xpath_context();
155
156   # Retrieve scalar metadata from DOM
157   foreach my $key ( keys %{$self->scalar_xpaths} ) {
158     my $xpath = ${$self->scalar_xpaths}{$key};
159     unless ( $xpath ) {
160       # Skip keys without xpath expression
161       ${$self->{_metadata}}{$key} = undef;
162       next;
163     }
164     my $value = $xc->find($xpath, $self->{dom});
165     if ( $value ) {
166       # Get rid of extraneous white space
167       $value = $value->string_value;
168       $value =~ s/\n|\r//g;
169       $value =~ s/\s{2,}/ /g;
170       ${$self->{_metadata}}{$key} = $value;
171     } else {
172       ${$self->{_metadata}}{$key} = undef;
173     }
174   }
175
176
177   # Convert payment code metadata field to Boolean
178   # See https://service.unece.org/trade/untdid/d16b/tred/tred4461.htm for other valid codes.
179   if (${$self->{_metadata}}{'direct_debit'}) {
180     ${$self->{_metadata}}{'direct_debit'} = ${$self->{_metadata}}{'direct_debit'} == 59 ? 1 : 0;
181   }
182
183   my @items;
184   $self->{_items} = \@items;
185
186   foreach my $item ( $xc->findnodes(ITEMS_XPATH, $self->{dom}) ) {
187     my %line_item;
188     foreach my $key ( keys %{$self->item_xpaths} ) {
189       my $xpath = ${$self->item_xpaths}{$key};
190       unless ( $xpath ) {
191         # Skip keys without xpath expression
192         $line_item{$key} = undef;
193         next;
194       }
195       my $value = $xc->find($xpath, $item);
196       if ( $value ) {
197         # Get rid of extraneous white space
198         $value = $value->string_value;
199         $value =~ s/\n|\r//g;
200         $value =~ s/\s{2,}/ /g;
201         $line_item{$key} = $value;
202       } else {
203         $line_item{$key} = undef;
204       }
205     }
206     push @items, \%line_item;
207   }
208
209 }
210
211 1;