1 package SL::XMLInvoice::CrossIndustryDocument;
6 use parent qw(SL::XMLInvoice::Base);
8 use constant ITEMS_XPATH => '//ram:IncludedSupplyChainTradeLineItem';
12 SL::XMLInvoice::CrossIndustryDocument - XML parser for UN/CEFACT Cross Industry Document
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.
21 See L<https://unece.org/trade/uncefact/xml-schemas> for that format's
26 This module is fairly simple. It keeps two hashes of XPath statements exposed
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.
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.
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.
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.
53 Johannes Grassler <info@computer-grassler.de>
58 my @supported = ( "UN/CEFACT Cross Industry Document/ZUGFeRD 1.0 (urn:ferd:CrossIndustryDocument:invoice:1p0)" );
63 my ($self, $dom) = @_;
65 my $rootnode = $dom->documentElement;
67 foreach my $attr ( $rootnode->attributes ) {
68 if ( $attr->getData =~ m/urn:ferd:CrossIndustryDocument:invoice:1p0/ ) {
76 # XML XPath expressions for global metadata
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'],
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'],
110 # Metadata accessor method
113 return $self->{_metadata};
116 # Item list accessor method
119 return $self->{_items};
123 my $xc = XML::LibXML::XPathContext->new;
124 $xc->registerNs(udt => 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:15');
125 $xc->registerNs(ram => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12');
126 $xc->registerNs(rsm => 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:1p0');
130 # Data keys we return
135 map { $keys{$_} = 1; } keys %{$self->scalar_xpaths};
140 # Item keys we return
145 map { $keys{$_} = 1; } keys %{$self->item_xpaths};
150 # Main parser subroutine for retrieving XML data
153 $self->{_metadata} = {};
154 $self->{_items} = ();
156 my $xc = _xpath_context();
158 # Retrieve scalar metadata from DOM
159 foreach my $key ( keys %{$self->scalar_xpaths} ) {
160 foreach my $xpath ( @{${$self->scalar_xpaths}{$key}} ) {
162 # Skip keys without xpath list
163 ${$self->{_metadata}}{$key} = undef;
166 my $value = $xc->find($xpath, $self->{dom});
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 ${$self->{_metadata}}{$key} = $value;
173 last; # first matching xpath wins
175 ${$self->{_metadata}}{$key} = undef;
181 # Convert payment code metadata field to Boolean
182 # See https://service.unece.org/trade/untdid/d16b/tred/tred4461.htm for other valid codes.
183 if (${$self->{_metadata}}{'direct_debit'}) {
184 ${$self->{_metadata}}{'direct_debit'} = ${$self->{_metadata}}{'direct_debit'} == 59 ? 1 : 0;
188 $self->{_items} = \@items;
190 foreach my $item ( $xc->findnodes(ITEMS_XPATH, $self->{dom}) ) {
192 foreach my $key ( keys %{$self->item_xpaths} ) {
193 foreach my $xpath ( @{${$self->item_xpaths}{$key}} ) {
195 # Skip keys without xpath list
196 $line_item{$key} = undef;
199 my $value = $xc->find($xpath, $item);
201 # Get rid of extraneous white space
202 $value = $value->string_value;
203 $value =~ s/\n|\r//g;
204 $value =~ s/\s{2,}/ /g;
205 $line_item{$key} = $value;
206 last; # first matching xpath wins
208 $line_item{$key} = undef;
212 push @items, \%line_item;