--- /dev/null
+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::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('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 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 ZUGFeRD invoice," .
+ " please ask your vendor to add this for his ZUGFeRD data.") unless $ustid;
+
+ $ustid =~ s/^\s+|\s+$//g;
+
+ # 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);
+ die t8("Please add a valid VAT-ID for this vendor: " . $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<action_upload_zugferd>
+
+Creates a web from with a single upload dialog.
+
+=item C<action_import_zugferd $pdf>
+
+Expects a single pdf with ZUGFeRD 2.0 metadata.
+Checks if the param <C$pdf> 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 E<lt>jan@kivitendo-premium.deE<gt>,
+
+=cut
'Correct taxkey' => 'Richtiger Steuerschlüssel',
'Cost Center' => 'Kostenstelle',
'Costs' => 'Kosten',
+ 'Could not extract ZUGFeRD data, data and error message:' => 'Konnte keine ZUGFeRD Daten extrahieren, folgende Fehlermeldung und das PDF:',
'Could not find an entry for this part in the pricegroup.' => 'Konnte keine Eintrag für diesen Artikel in der Preisgruppe finden.',
'Could not load class #1 (#2): "#3"' => 'Konnte Klasse #1 (#2) nicht laden: "#3"',
'Could not load class #1, #2' => 'Konnte Klasse #1 nicht laden: "#2"',
'Import CSV' => 'CSV-Import',
'Import Status' => 'Import Status',
'Import a MT940 file:' => 'Laden Sie eine MT940 Datei hoch:',
+ 'Import a ZUGFeRD file:' => 'Eine ZUGFeRD-Datei importieren',
'Import all' => 'Importiere Alle',
'Import documents from #1' => 'Importiere Dateien von Quelle \'#1\'',
'Import file' => 'Import-Datei',
'Next run at' => 'Nächste Ausführung um',
'No' => 'Nein',
'No 1:n or n:1 relation' => 'Keine 1:n oder n:1 Beziehung',
+ 'No AP Record Template for this vendor found, please add one' => 'Konnte keine Kreditorenbuchungsvorlage für diesen Lieferanten finden, bitte legen Sie eine an.',
'No AP template was found.' => 'Keine Kreditorenbuchungsvorlage gefunden.',
'No Company Address given' => 'Keine Firmenadresse hinterlegt!',
'No Company Name given' => 'Kein Firmenname hinterlegt!',
'No Journal' => 'Kein Journal',
'No Shopdescription' => 'Keine Shop-Artikelbeschreibung',
'No Shopimages' => 'Keine Shop-Bilder',
+ 'No VAT Info for this ZUGFeRD invoice, please ask your vendor to add this for his ZUGFeRD data.' => 'Konnte keine UST-ID für diese ZUGFeRD Rechnungen finden, bitte fragen Sie bei Ihren Lieferanten nach, ob dieses Feld im ZUGFeRD Datensatz gesetzt wird.',
'No Vendor was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Händler gefunden',
'No action defined.' => 'Keine Aktion definiert.',
'No article has been selected yet.' => 'Es wurde noch kein Artikel ausgewählt.',
'Pictures for search parts' => 'Bilder für Warensuche',
'Please Check the bank information for each customer:' => 'Bitte überprüfen Sie die Bankinformationen der Kunden:',
'Please Check the bank information for each vendor:' => 'Bitte überprüfen Sie die Kontoinformationen der Lieferanten:',
+ 'Please add a valid VAT-ID for this vendor: ' => 'Bitte prüfen Sie ob dieser Lieferant eine valide UST-ID (Großschreibungen und Leerzeichen beachten) besitzt:',
'Please ask your administrator to create warehouses and bins.' => 'Bitten Sie Ihren Administrator, dass er Lager und Lagerplätze anlegt.',
'Please change the partnumber of the following parts and run the update again:' => 'Bitte ändern Sie daher die Artikelnummer folgender Artikel:',
'Please choose a part.' => 'Bitte wählen Sie einen Artikel aus.',
'Your download does not exist anymore. Please re-run the DATEV export assistant.' => 'Ihr Download existiert nicht mehr. Bitte starten Sie den DATEV-Exportassistenten erneut.',
'Your import is being processed.' => 'Ihr Import wird verarbeitet',
'Your target quantity will be added to the stocked quantity.' => 'Ihre gezählte Zielmenge wird zum Lagerbestand hinzugezählt.',
+ 'ZUGFeRD import' => 'ZUGFeRD Import',
'ZUGFeRD invoice' => 'ZUGFeRD-Rechnung',
'ZUGFeRD notes for each invoice' => 'ZUGFeRD-Notizen für jede Rechnung',
'Zeitraum' => 'Zeitraum',
'Zip, City' => 'PLZ, Ort',
'Zipcode' => 'PLZ',
'Zipcode and city' => 'PLZ und Stadt',
+ 'ZugFeRD Import' => 'ZUGFeRD Import',
'[email]' => '[email]',
'absolute' => 'absolut',
'account_description' => 'Beschreibung',
'brutto' => 'brutto',
'building data' => 'Verarbeite Daten',
'building report' => 'Erstelle Bericht',
+ 'can only parse a pdf file' => 'Kann nur eine gültige PDF-Datei verwenden.',
'cash' => 'Ist-Versteuerung',
'chargenumber #1' => 'Chargennummer #1',
'chart_of_accounts' => 'kontenuebersicht',
'male' => 'männlich',
'max filesize' => 'maximale Dateigröße',
'missing' => 'Fehlbestand',
+ 'missing file for action import' => 'Es wurde keine Datei zum Hochladen ausgewählt',
'missing_br' => 'Fehl.',
'month' => 'Monatliche Abgabe',
'monthly' => 'monatlich',