X-Git-Url: http://wagnertech.de/git?p=kivitendo-erp.git;a=blobdiff_plain;f=SL%2FController%2FZUGFeRD.pm;fp=SL%2FController%2FZUGFeRD.pm;h=699e5fe5ef7ac3e294c4a2bcde17219fff82419d;hp=0000000000000000000000000000000000000000;hb=53593baa211863fbf66540cf1bcc36c8fb37257f;hpb=deb4d2dbb676d7d6f69dfe7815d6e0cb09bd4a44 diff --git a/SL/Controller/ZUGFeRD.pm b/SL/Controller/ZUGFeRD.pm new file mode 100644 index 000000000..699e5fe5e --- /dev/null +++ b/SL/Controller/ZUGFeRD.pm @@ -0,0 +1,225 @@ +package SL::Controller::ZUGFeRD; +use strict; +use parent qw(SL::Controller::Base); + +use SL::DB::RecordTemplate; +use SL::Locale::String qw(t8); +use SL::Helper::DateTime; +use SL::VATIDNr; +use SL::ZUGFeRD; + +use XML::LibXML; + + +__PACKAGE__->run_before('check_auth'); + +sub action_upload_zugferd { + my ($self, %params) = @_; + + $self->setup_zugferd_action_bar; + $self->render('zugferd/form', title => $::locale->text('Factur-X/ZUGFeRD import')); +} + +sub action_import_zugferd { + my ($self, %params) = @_; + + die t8("missing file for action import") unless $::form->{file}; + die t8("can only parse a pdf file") unless $::form->{file} =~ m/^%PDF/; + + my $info = SL::ZUGFeRD->extract_from_pdf($::form->{file}); + + if ($info->{result} != SL::ZUGFeRD::RES_OK()) { + # An error occurred; log message from parser: + $::lxdebug->message(LXDebug::DEBUG1(), "Could not extract ZUGFeRD data, error message: " . $info->{message}); + die t8("Could not extract Factur-X/ZUGFeRD data, data and error message:") . $info->{message}; + } + # valid ZUGFeRD metadata + my $dom = XML::LibXML->load_xml(string => $info->{invoice_xml}); + + # 1. check if ZUGFeRD SellerTradeParty has a VAT-ID + my $ustid = $dom->findnodes('//ram:SellerTradeParty/ram:SpecifiedTaxRegistration')->string_value; + die t8("No VAT Info for this Factur-X/ZUGFeRD invoice," . + " please ask your vendor to add this for his Factur-X/ZUGFeRD data.") unless $ustid; + + $ustid = SL::VATIDNr->normalize($ustid); + + # 1.1 check if we a have a vendor with this VAT-ID (vendor.ustid) + my $vc = $dom->findnodes('//ram:SellerTradeParty/ram:Name')->string_value; + my $vendor = SL::DB::Manager::Vendor->find_by( + ustid => $ustid, + or => [ + obsolete => undef, + obsolete => 0, + ]); + + if (!$vendor) { + # 1.2 If no vendor with the exact VAT ID number is found, the + # number might be stored slightly different in the database + # (e.g. with spaces breaking up groups of numbers). Iterate over + # all existing vendors with VAT ID numbers, normalize their + # representation and compare those. + + my $vendors = SL::DB::Manager::Vendor->get_all( + where => [ + '!ustid' => undef, + '!ustid' => '', + or => [ + obsolete => undef, + obsolete => 0, + ], + ]); + + foreach my $other_vendor (@{ $vendors }) { + next unless SL::VATIDNr->normalize($other_vendor->ustid) eq $ustid; + + $vendor = $other_vendor; + last; + } + } + + die t8("Please add a valid VAT-ID for this vendor: #1", $vc) unless (ref $vendor eq 'SL::DB::Vendor'); + + # 2. check if we have a ap record template for this vendor (TODO only the oldest template is choosen) + my $template_ap = SL::DB::Manager::RecordTemplate->get_first(where => [vendor_id => $vendor->id]); + die t8("No AP Record Template for this vendor found, please add one") unless (ref $template_ap eq 'SL::DB::RecordTemplate'); + + + # 3. parse the zugferd data and fill the ap record template + # -> no need to check sign (credit notes will be negative) just record thei ZUGFeRD type in ap.notes + # -> check direct debit (defaults to no) + # -> set amount (net amount) and unset taxincluded + # (template and user cares for tax and if there is more than one booking accno) + # -> date (can be empty) + # -> duedate (may be empty) + # -> compare record iban and generate a warning if this differs from vendor's master data iban + my $total = $dom->findnodes('//ram:SpecifiedTradeSettlementHeaderMonetarySummation' . + '/ram:TaxBasisTotalAmount')->string_value; + + my $invnumber = $dom->findnodes('//rsm:ExchangedDocument/ram:ID')->string_value; + + # parse dates to kivi if set/valid + my ($transdate, $duedate, $dt_to_kivi, $due_dt_to_kivi); + $transdate = $dom->findnodes('//ram:IssueDateTime')->string_value; + $duedate = $dom->findnodes('//ram:DueDateDateTime')->string_value; + $transdate =~ s/^\s+|\s+$//g; + $duedate =~ s/^\s+|\s+$//g; + + if ($transdate =~ /^[0-9]{8}$/) { + $dt_to_kivi = DateTime->new(year => substr($transdate,0,4), + month => substr ($transdate,4,2), + day => substr($transdate,6,2))->to_kivitendo; + } + if ($duedate =~ /^[0-9]{8}$/) { + $due_dt_to_kivi = DateTime->new(year => substr($duedate,0,4), + month => substr ($duedate,4,2), + day => substr($duedate,6,2))->to_kivitendo; + } + + my $type = $dom->findnodes('//rsm:ExchangedDocument/ram:TypeCode')->string_value; + + my $dd = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement' . + '/ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode')->string_value; + my $direct_debit = $dd == 59 ? 1 : 0; + + my $iban = $dom->findnodes('//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementPaymentMeans' . + '/ram:PayeePartyCreditorFinancialAccount/ram:IBANID')->string_value; + my $ibanmessage; + $ibanmessage = $iban ne $vendor->iban ? "Record IBAN $iban doesn't match vendor IBAN " . $vendor->iban : $iban if $iban; + + my $url = $self->url_for( + controller => 'ap.pl', + action => 'load_record_template', + id => $template_ap->id, + 'form_defaults.amount_1' => $::form->format_amount(\%::myconfig, $total, 2), + 'form_defaults.transdate' => $dt_to_kivi, + 'form_defaults.invnumber' => $invnumber, + 'form_defaults.duedate' => $due_dt_to_kivi, + 'form_defaults.no_payment_bookings' => 0, + 'form_defaults.paid_1_suggestion' => $::form->format_amount(\%::myconfig, $total, 2), + 'form_defaults.notes' => "ZUGFeRD Import. Type: $type\nIBAN: " . $ibanmessage, + 'form_defaults.taxincluded' => 0, + 'form_defaults.direct_debit' => $direct_debit, + ); + + $self->redirect_to($url); + +} + +sub check_auth { + $::auth->assert('ap_transactions'); +} +sub setup_zugferd_action_bar { + my ($self) = @_; + + for my $bar ($::request->layout->get('actionbar')) { + $bar->add( + action => [ + $::locale->text('Import'), + submit => [ '#form', { action => 'ZUGFeRD/import_zugferd' } ], + accesskey => 'enter', + ], + ); + } +} + + +1; +__END__ + +=pod + +=encoding utf8 + +=head1 NAME + +SL::Controller::ZUGFeRD +Controller for importing ZUGFeRD pdf files to kivitendo + +=head1 FUNCTIONS + +=over 4 + +=item C + +Creates a web from with a single upload dialog. + +=item C + +Expects a single pdf with ZUGFeRD 2.0 metadata. +Checks if the param is set and a valid pdf file. +Calls helper functions to validate and extract the ZUGFeRD data. +Needs a valid VAT ID (EU) for this vendor and +expects one ap template for this vendor in kivitendo. + +Parses some basic ZUGFeRD data (invnumber, total net amount, +transdate, duedate, vendor VAT ID, IBAN) and uses the first +found ap template for this vendor to fill this template with +ZUGFeRD data. +If the vendor's master data contain a IBAN and the +ZUGFeRD record has a IBAN also these values will be compared. +If they don't match a warning will be writte in ap.notes. +Furthermore the ZUGFeRD type code will be written to ap.notes. +No callback implemented. + +=back + +=head1 TODO and CAVEAT + +This is just a very basic Parser for ZUGFeRD data. +We assume that the ZUGFeRD generator is a company with a +valid European VAT ID. Furthermore this vendor needs only +one and just noe ap template (the first match will be used). + +The ZUGFeRD data should also be extracted in the helper package +and maybe a model should be used for this. +The user should set one ap template as a default for ZUGFeRD. +The ZUGFeRD pdf should be written to WebDAV or DMS. +If the ZUGFeRD data has a payment purpose set, this should +be the default for the SEPA-XML export. + + +=head1 AUTHOR + +Jan Büren Ejan@kivitendo-premium.deE, + +=cut