#!/usr/bin/perl -w use strict; use FindBin qw($Bin); use lib $Bin; use wtr2_base; my $DEBUG=0; init_db(); my $CAN_OUTPUT= 1; my @files= @ARGV || (<$dir{invoices}/*.xml>); foreach my $file (@files) { # XMLin just re-implements a simplified version of XML::Simple's XMLin # that will work for this specific class of document my $pyx= pyx_in( $file); my $xml= XMLin( $pyx, forcearray => [ qw(InvoiceRow)]); my $errors= check_invoice( $xml); if( !@$errors) { store_invoice( $xml); } else { print "ERROR in $file\n ", join( "\n ", @$errors), "\n"; if( $CAN_OUTPUT) { my $rejected_file= rejected( $file); print "adding errors in $rejected_file\n" if( $DEBUG); add_errors( $pyx, $errors); output_doc_to_check( $rejected_file, $pyx); } } } exit; sub check_invoice { my( $xml)= @_; my $errors=[]; # array ref, holds the error messages check_buyer( $xml->{BuyerPartyDetails}->{BuyerPartyIdentifier}->{content}, $xml->{BuyerPartyDetails}->{BuyerOrganisationName}->{content}, $errors ); check_po( $xml->{InvoiceDetails}->{OrderIdentifier}->{content}, $errors); my @rows= @{$xml->{InvoiceRow}}; reset_default_row_id(); foreach my $row( @rows) { # this does not cope well with broken row numbers my $row_id= $row->{RowIdentifier}->{content} || default_row_id(); print "checking row $row_id\n" if $DEBUG; check_qtty( $row_id, $row->{DeliveredQuantity}->{content}, $row->{DeliveredQuantity}->{QuantityUnitCode}, $row->{OrderedQuantity}->{content}, $row->{OrderedQuantity}->{QuantityUnitCode}, $errors ); } return $errors; } sub store_invoice { my( $xml)= @_; print "storing invoice $xml->{InvoiceDetails}->{InvoiceNumber}->{content}\n"; # build the various data structures my $data; my $invoice = $xml->{InvoiceDetails}; $data->{invoice} = { number => $invoice->{InvoiceNumber}->{content}, date => $invoice->{InvoiceDate}->{content}, po => $invoice->{OrderIdentifier}->{content}, amount_no_tax => $invoice->{InvoiceTotalVatExcludedAmount}->{content}, tax => $invoice->{InvoiceTotalVatAmount}->{content}, amount => $invoice->{InvoiceTotalVatIncludedAmount}->{content}, payment_status => $xml->{PaymentStatusDetails}->{PaymentStatusCode}->{content}, }; my $seller = $xml->{SellerPartyDetails}; $data->{seller} = { identifier => $seller->{SellerPartyIdentifier}->{content}, name => $seller->{SellerOrganisationName}->{content}, tax_code => $seller->{SellerOrganisationTaxCode}->{content}, }; my $address = $xml->{SellerPartyDetails}->{SellerPostalAddressDetails}; $data->{address} = { street => $address->{SellerStreetName}->{content}, town => $address->{SellerTownName}->{content}, zip => $address->{SellerPostCodeIdentifier}->{content}, country_code => $address->{CountryCode}->{content}, po_box => $address->{SellerPostOfficeBoxIdentifier}->{content}, }; $data->{contact} = { name => $xml->{SellerContactPersonName}->{content}, phone => $xml->{SellerCommunicationDetails}->{SellerPhoneNumberIdentifier}->{content}, email => $xml->{SellerCommunicationDetails}->{SellerEmailaddressIdentifier}->{content}, }; $data->{invoicerow} ||= []; reset_default_row_id(); foreach my $invoicerow (@{$xml->{InvoiceRow}}) { push @{$data->{invoicerow}}, { row_id => $invoicerow->{RowIdentifier}->{content} || default_row_id(), sku => $invoicerow->{ArticleIdentifier}->{content}, name => $invoicerow->{ArticleName}->{content}, qty => $invoicerow->{DeliveredQuantity}->{content}, qty_unit => $invoicerow->{DeliveredQuantity}->{QuantityUnitCode}, unit_price => $invoicerow->{UnitPriceAmount}->{content}, amount_no_tax => $invoicerow->{RowVatExcludedAmount}->{content}, tax => $invoicerow->{RowVatAmount}->{content}, amount => $invoicerow->{RowAmount}->{content}, } } store_all( $data); } sub add_errors { my( $pyx, $error_messages)= @_; # first get the root element my $root_index=0; while($_=$pyx->[$root_index]) { if( m{^\(Finvoice$}) { # found the root element # now skip attributes while( $pyx->[$root_index+1]=~ m{^A}) { $root_index++; } last; } $root_index++; } my $lf= "-\\n\n"; # a line feed in pyx # build the error messages my $messages= $lf . "- \n(errors\n" . join( "", map { "$lf- \n(error\n-$_\n)error\n" } @$error_messages) . $lf . "- \n)errors\n"; # now insert the messages splice( @$pyx, $root_index+1, 0, $messages); return $pyx; } sub output_doc_to_check { my( $file, $pyx)= @_; open( FILE, "| pyxw > $file") or die "cannot create file to check $file: $!"; print FILE @$pyx; close FILE; } sub pyx_in { my( $file)= @_; open( IN, "pyx $file | ") or die "cannot open pyx $file: $!"; my @pyx= ; # note that in case there is an error during the parsing it will show when # closing the file, so you need to check the result of close close IN or die "error processing pyx $file: $!"; return \@pyx; } sub XMLin { my( $pyx, %options)=@_; # get the 'forcearray' elements in a convenient hash my %forcearray= map { $_ => 1} @{$options{forcearray}}; my @current; # the stack of hashes or array in the data structure # $current[0] is the overall structure (the root of the document), # $current[1] is the first_level child currently open # ... # $current[-1] is the current element foreach (@$pyx) { if( m{^\((.*)$}) # open element { my $tag= $1; my $current={}; # will be filled later; if( @current) { # normal (non-root) element if( $forcearray{$1}) { $current[-1]->{$1} ||= []; push @{$current[-1]->{$1}}, $current; } else { $current[-1]->{$1} = $current; } } push @current, $current; } elsif( m{^\)(.*)$}) # close element (except when only the root is left) { pop @current unless( @current == 1); } elsif( m{^-\\n$}) # empty line, skip { } elsif( m{^-(.*)$}) # content, assign to the current element content { $current[-1]->{content}= $1; } elsif( m{^A(\w*) (.*)$}) # attribute, assign to a field in the current element { $current[-1]->{$1}= $2; } } return $current[0]; } __END__ =head1 NAME wtr2_pyx_simple_plus =head1 SYNOPSYS perl wtr2_pyx_simple_plus =head1 DESCRIPTION This example uses L and the L layer on top of it, but stores the initial PYX flow so it can later update it with the error messages. The code is very similar to wtr2_pyx_simple, but the PYX flow if first stored in C<$pyx>, before being passed to XMLin. If errors are found, C<$pyx> is used to add the errors (L looks for the end of the root (C) start tag, builds the PYX flow for the C element and insert it after the root. The pyx flow is then output using C which writes it back as XML. Creating the PYX for the error element is not really elegant, but it is not that much of a pain either. A simple layer a-la-L could make it even easier, but wasn't deemed necessary here. =head1 AUTHOR Michel Rodriguez =head1 LICENSE This code is Copyright (c) 2003 Michel Rodriguez. All rights reserved. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. Comments can be sent to mirod@xmltwig.com =head1 SEE ALSO L, L, wtr2_simple Ways to Rome 2 - Kourallinen Dollareita: http://www.xmltwig.com/article/ways_to_rome_2/