MT940-Import: Implementation eines eigenen Parsers anstelle von AQBanking
authorMoritz Bunkus <m.bunkus@linet.de>
Tue, 3 Nov 2020 16:53:37 +0000 (17:53 +0100)
committerMoritz Bunkus <m.bunkus@linet.de>
Tue, 3 Nov 2020 16:53:37 +0000 (17:53 +0100)
SL/Controller/BankImport.pm
SL/MT940.pm [new file with mode: 0644]
locale/de/all
templates/webpages/bank_import/import_mt940.html [new file with mode: 0644]
templates/webpages/bank_import/upload_mt940.html [new file with mode: 0644]
templates/webpages/bankimport/form.html [deleted file]

index bc65d17..04d72d3 100644 (file)
@@ -1,16 +1,23 @@
 package SL::Controller::BankImport;
+
 use strict;
-use Data::Dumper;
+
 use parent qw(SL::Controller::Base);
 
-use SL::Locale::String qw(t8);
-use SL::DB::CsvImportProfile;
-use SL::Helper::MT940;
+use List::MoreUtils qw(apply);
+use List::Util qw(max min);
+
+use SL::DB::BankAccount;
+use SL::DB::BankTransaction;
+use SL::DB::Default;
+use SL::Helper::Flash;
+use SL::MT940;
 use SL::SessionFile::Random;
 
 use Rose::Object::MakeMethods::Generic
 (
- 'scalar --get_set_init' => [ qw(profile) ],
+  scalar                  => [ qw(file_name transactions statistics) ],
+  'scalar --get_set_init' => [ qw(bank_accounts) ],
 );
 
 __PACKAGE__->run_before('check_auth');
@@ -19,38 +26,175 @@ sub action_upload_mt940 {
   my ($self, %params) = @_;
 
   $self->setup_upload_mt940_action_bar;
-  $self->render('bankimport/form', title => $::locale->text('MT940 import'), profile => $self->profile ? 1 : 0);
+  $self->render('bank_import/upload_mt940', title => $::locale->text('MT940 import'));
 }
 
-sub action_import_mt940 {
+sub action_import_mt940_preview {
   my ($self, %params) = @_;
 
-  die "missing file for action import" unless $::form->{file};
+  if (!$::form->{file}) {
+    flash_later('error', $::locale->text('You have to upload an MT940 file to import.'));
+    return $self->redirect_to(action => 'upload_mt940');
+  }
 
-  my $converted_data = SL::Helper::MT940::convert_mt940_data($::form->{file});
+  die "missing file for action import_mt940_preview" unless $::form->{file};
 
-  # store the converted data in a session file and create a temporary profile with it's name
   my $file = SL::SessionFile::Random->new(mode => '>');
-  $file->fh->print($converted_data);
+  $file->fh->print($::form->{file});
   $file->fh->close;
-  $self->profile->set('file_name', $file->file_name);
-  $self->profile($self->profile->clone_and_reset_deep)->save;
 
-  die t8("The MT940 import needs an import profile called MT940") unless $self->profile;
+  $self->file_name($file->file_name);
+  $self->parse_and_analyze_transactions;
+
+  $self->setup_upload_mt940_preview_action_bar;
+  $self->render('bank_import/import_mt940', title => $::locale->text('MT940 import preview'), preview => 1);
+}
+
+sub action_import_mt940 {
+  my ($self, %params) = @_;
+
+  die "missing file for action import_mt940" unless $::form->{file_name};
+
+  $self->file_name($::form->{file_name});
+  $self->parse_and_analyze_transactions;
+  $self->import_transactions;
+
+  $self->render('bank_import/import_mt940', title => $::locale->text('MT940 import result'));
+}
+
+sub parse_and_analyze_transactions {
+  my ($self, %params) = @_;
+
+  my $errors     = 0;
+  my $duplicates = 0;
+  my ($min_date, $max_date);
 
-  $self->redirect_to(controller => 'controller.pl', action => 'CsvImport/test', 'profile.type' => 'bank_transactions', 'profile.id' => $self->profile->id, force_profile => 1);
+  my $currency_id = SL::DB::Default->get->currency_id;
+
+  $self->transactions([ sort { $a->{transdate} cmp $b->{transdate} } SL::MT940->parse($self->file_name) ]);
+
+  foreach my $transaction (@{ $self->transactions }) {
+    $transaction->{bank_account}   = $self->bank_accounts->{ make_bank_account_idx($transaction->{local_bank_code}, $transaction->{local_account_number}) };
+    $transaction->{bank_account} //= $self->bank_accounts->{ make_bank_account_idx('IBAN',                          $transaction->{local_account_number}) };
+
+    if (!$transaction->{bank_account}) {
+      $transaction->{error} = $::locale->text('No bank account configured for bank code/BIC #1, account number/IBAN #2.', $transaction->{local_bank_code}, $transaction->{local_account_number});
+      $errors++;
+      next;
+    }
+
+    $transaction->{local_bank_account_id} = $transaction->{bank_account}->id;
+    $transaction->{currency_id}           = $currency_id;
+
+    $min_date = min($min_date // $transaction->{transdate}, $transaction->{transdate});
+    $max_date = max($max_date // $transaction->{transdate}, $transaction->{transdate});
+  }
+
+  my %existing_bank_transactions;
+
+  if ((scalar(@{ $self->transactions }) - $errors) > 0) {
+    my @entries =
+      @{ SL::DB::Manager::BankTransaction->get_all(
+          where => [
+            transdate => { ge => $min_date },
+            transdate => { lt => $max_date->clone->add(days => 1) },
+          ],
+          inject_results => 1) };
+
+    %existing_bank_transactions = map { (make_transaction_idx($_) => 1) } @entries;
+  }
+
+  foreach my $transaction (@{ $self->transactions }) {
+    next if $transaction->{error};
+
+    if ($existing_bank_transactions{make_transaction_idx($transaction)}) {
+      $transaction->{duplicate} = 1;
+      $duplicates++;
+      next;
+    }
+  }
+
+  $self->statistics({
+    total      => scalar(@{ $self->transactions }),
+    errors     => $errors,
+    duplicates => $duplicates,
+    to_import  => scalar(@{ $self->transactions }) - $errors - $duplicates,
+  });
+}
+
+sub import_transactions {
+  my ($self, %params) = @_;
+
+  my $imported = 0;
+
+  SL::DB::client->with_transaction(sub {
+    # make Emacs happy
+    1;
+
+    foreach my $transaction (@{ $self->transactions }) {
+      next if $transaction->{error} || $transaction->{duplicate};
+
+      SL::DB::BankTransaction->new(
+        map { ($_ => $transaction->{$_}) } qw(amount currency_id local_bank_account_id purpose remote_account_number remote_bank_code remote_name transaction_code transdate valutadate)
+      )->save;
+
+      $imported++;
+    }
+
+    1;
+  });
+
+  $self->statistics->{imported} = $imported;
 }
 
 sub check_auth {
   $::auth->assert('bank_transaction');
 }
 
-sub init_profile {
-  my $profile = SL::DB::Manager::CsvImportProfile->find_by(name => 'MT940', login => $::myconfig{login});
-  if ( ! $profile ) {
-    $profile = SL::DB::Manager::CsvImportProfile->find_by(name => 'MT940', login => 'default');
+sub make_bank_account_idx {
+  return join '/', map { my $q = $_; $q =~ s{ +}{}g; $q } @_;
+}
+
+sub normalize_text {
+  my ($text) = @_;
+
+  $text = lc($text // '');
+  $text =~ s{ }{}g;
+
+  return $text;
+}
+
+sub make_transaction_idx {
+  my ($transaction) = @_;
+
+  if (ref($transaction) eq 'SL::DB::BankTransaction') {
+    $transaction = { map { ($_ => $transaction->$_) } qw(local_bank_account_id transdate valutadate amount purpose) };
   }
-  return $profile;
+
+  return normalize_text(join '/',
+                        map { $_ // '' }
+                        ($transaction->{local_bank_account_id},
+                         $transaction->{transdate}->ymd,
+                         $transaction->{valutadate}->ymd,
+                         (apply { s{0+$}{} } $transaction->{amount} * 1),
+                         $transaction->{purpose}));
+}
+
+sub init_bank_accounts {
+  my ($self) = @_;
+
+  my %bank_accounts;
+
+  foreach my $bank_account (@{ SL::DB::Manager::BankAccount->get_all }) {
+    if ($bank_account->bank_code && $bank_account->account_number) {
+      $bank_accounts{make_bank_account_idx($bank_account->bank_code, $bank_account->account_number)} = $bank_account;
+    }
+    if ($bank_account->iban) {
+      $bank_accounts{make_bank_account_idx('IBAN', $bank_account->iban)} = $bank_account;
+    }
+  }
+
+  return \%bank_accounts;
 }
 
 sub setup_upload_mt940_action_bar {
@@ -60,8 +204,23 @@ sub setup_upload_mt940_action_bar {
     $bar->add(
       action => [
         $::locale->text('Preview'),
+        submit    => [ '#form', { action => 'BankImport/import_mt940_preview' } ],
+        accesskey => 'enter',
+      ],
+    );
+  }
+}
+
+sub setup_upload_mt940_preview_action_bar {
+  my ($self) = @_;
+
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        $::locale->text('Import'),
         submit    => [ '#form', { action => 'BankImport/import_mt940' } ],
         accesskey => 'enter',
+        disabled  => $self->statistics->{to_import} ? undef : $::locale->text('No entries can be imported.'),
       ],
     );
   }
diff --git a/SL/MT940.pm b/SL/MT940.pm
new file mode 100644 (file)
index 0000000..d96a9f2
--- /dev/null
@@ -0,0 +1,122 @@
+package SL::MT940;
+
+use strict;
+use warnings;
+
+use Data::Dumper;
+use DateTime;
+use Encode;
+use File::Slurp qw(read_file);
+
+sub _join_entries {
+  my ($parts, $from, $to, $separator) = @_;
+
+  $separator //= ' ';
+
+  return
+    join $separator,
+    grep { $_ }
+    map  { s{^\s+|\s+$}{}g; $_ }
+    grep { $_ }
+    map  { $parts->{$_} }
+    ($from..$to);
+}
+
+sub parse {
+  my ($class, $file_name) = @_;
+
+  my ($local_bank_code, $local_account_number, %transaction, @transactions, @lines);
+  my $line_number = 0;
+
+  my $store_transaction = sub {
+    if (%transaction) {
+      push @transactions, { %transaction };
+      %transaction = ();
+    }
+  };
+
+  foreach my $line (read_file($file_name)) {
+    chomp $line;
+    $line = Encode::decode('UTF-8', $line);
+    $line =~ s{\r+}{};
+    $line_number++;
+
+    if (@lines && ($line =~ m{^\%})) {
+      $lines[-1]->[0] .= substr($line, 1);
+
+    } else {
+      push @lines, [ $line, $line_number ];
+    }
+  }
+
+  foreach my $line (@lines) {
+    if ($line->[0] =~ m{^:25:(\d+)/(\d+)}) {
+      $local_bank_code      = $1;
+      $local_account_number = $2;
+
+    } elsif ($line->[0] =~ m{^:61: (\d{2}) (\d{2}) (\d{2}) (\d{2}) (\d{2}) (C|D|RC|RD) (.) (\d+) (?:, (\d*))? N (.{3}) (.*)}x) {
+      #                       1       2       3       4       5       6                7   8          9         10     11
+      # :61:2008060806CR952,N051NONREF
+
+      $store_transaction->();
+
+      my $valuta_year      = $1 * 1 + 2000;
+      my $valuta_month     = $2;
+      my $valuta_day       = $3;
+      my $trans_month      = $4;
+      my $trans_day        = $5;
+      my $debit_credit     = $6;
+      my $currency         = $7;
+      my $amount1          = $8;
+      my $amount2          = $9 || 0;
+      my $transaction_code = $10;
+      my $reference        = $11;
+
+      my $valuta_date      = DateTime->new_local(year => $valuta_year, month => $valuta_month, day => $valuta_day);
+      my $trans_date       = DateTime->new_local(year => $valuta_year, month => $trans_month,  day => $trans_day);
+      my $diff             = $valuta_date->subtract_datetime($trans_date);
+      my $trans_year_diff  = $diff->months < 6           ?  0
+                           : $valuta_date  > $trans_date ?  1
+                           :                               -1;
+      $trans_date          = DateTime->new_local(year => $valuta_year + $trans_year_diff, month => $trans_month,  day => $trans_day);
+      my $sign             = ($debit_credit eq 'D') || ($debit_credit eq 'RC') ? -1 : 1;
+      $reference           =~ s{//.*}{};
+      $reference           = '' if $reference eq 'NONREF';
+
+      %transaction = (
+        line_number          => $line->[1],
+        currency             => $currency,
+        valutadate           => $valuta_date,
+        transdate            => $trans_date,
+        amount               => ($amount1 * 1 + ($amount2 / (10 ** length($amount2))))* $sign,
+        reference            => $reference,
+        transaction_code     => $transaction_code,
+        local_bank_code      => $local_bank_code,
+        local_account_number => $local_account_number,
+      );
+
+    } elsif (%transaction && ($line->[0] =~ m{^:86:})) {
+      if ($line->[0] =~ m{^:86:\d+\?(.+)}) {
+        # structured
+        my %parts = map { ((substr($_, 0, 2) // '0') * 1 => substr($_, 2)) } split m{\?}, $1;
+
+        $transaction{purpose}               = _join_entries(\%parts, 20, 29);
+        $transaction{remote_name}           = _join_entries(\%parts, 32, 33, '');
+        $transaction{remote_bank_code}      = $parts{30};
+        $transaction{remote_account_number} = $parts{31};
+
+      } else {
+        # unstructured
+        $transaction{purpose} = substr($line->[0], 5);
+      }
+
+      $store_transaction->();
+    }
+  }
+
+  $store_transaction->();
+
+  return @transactions;
+}
+
+1;
index 3a2609b..6ebd46c 100755 (executable)
@@ -278,6 +278,7 @@ $self->{texts} = {
   'Allow the following users access to my follow-ups:' => 'Erlaube den folgenden Benutzern Zugriff auf meine Wiedervorlagen:',
   'Allow to delete generated printfiles' => 'Löschen von erzeugten Dokumenten erlaubt',
   'Already counted'             => 'Bereits erfasst',
+  'Already imported entries (duplicates)' => 'Bereits importierte Einträge (Duplikate)',
   'Always edit assembly items (user can change/delete items even if assemblies are already produced)' => 'Erzeugnisbestandteile verändern (Löschen/Umsortieren) auch nachdem dieses Erzeugnis schon produziert wurde.',
   'Always save orders with a projectnumber (create new projects)' => 'Aufträge immer mit Projektnummer speichern (neue Projekte erstellen)',
   'Amended Advance Turnover Tax Return' => 'Berichtigte Anmeldung',
@@ -1134,6 +1135,7 @@ $self->{texts} = {
   'Dunnings'                    => 'Mahnungen',
   'Dunnings (Id -- Dunning Date --Dunning Level -- Dunning Fee)' => 'Mahnungen (Nummer -- Mahndatum -- Mahnstufe -- Mahngebühr/Zinsen)',
   'Dunningstatistic'            => 'Mahnstatistik',
+  'Duplicate'                   => 'Duplikat',
   'Duplicate in CSV file'       => 'Duplikat in CSV-Datei',
   'Duplicate in database'       => 'Duplikat in Datenbank',
   'During the next update a taxkey 0 with tax rate of 0 will automatically created.' => 'Beim nächsten Ausführen des Updates wird ein Steuerschlüssel 0 mit einem Steuersatz von 0% automatisch erzeugt.',
@@ -1275,6 +1277,8 @@ $self->{texts} = {
   'Enter the requested execution date or leave empty for the quickest possible execution:' => 'Geben Sie das jeweils gewünschte Ausführungsdatum an, oder lassen Sie das Feld leer für die schnellstmögliche Ausführung:',
   'Entries for which automatic conversion failed:' => 'Einträge, für die die automatische Umstellung fehlschlug:',
   'Entries for which automatic conversion succeeded:' => 'Einträge, für die die automatische Umstellung erfolgreich war:',
+  'Entries ready to import'     => 'Zu importierende Einträge',
+  'Entries with errors'         => 'Einträge mit Fehlern',
   'Equity'                      => 'Passiva',
   'Erfolgsrechnung'             => 'Erfolgsrechnung',
   'Error'                       => 'Fehler',
@@ -1314,7 +1318,6 @@ $self->{texts} = {
   'Error: Invalid language'     => 'Fehler: Sprache ungültig',
   'Error: Invalid part'         => 'Fehler: Artikel ungültig',
   'Error: Invalid part type'    => 'Fehler: ungültiger Artikeltyp',
-  'Error: Invalid parts group'  => 'Fehler: Warengruppe ungültig',
   'Error: Invalid parts group id #1' => 'Fehler: Ungültige Warengruppen-ID #1',
   'Error: Invalid parts group name #1' => 'Fehler: Ungültiger Warengruppenname: #1',
   'Error: Invalid payment terms' => 'Fehler: Zahlungsbedingungen ungültig',
@@ -1668,6 +1671,8 @@ $self->{texts} = {
   'Import result'               => 'Import-Ergebnis',
   'Import scanned documents'    => 'Importiere gescannte Dateien',
   'Importdate'                  => 'Importdatum',
+  'Imported'                    => 'Importiert',
+  'Imported entries'            => 'Importierte Einträge',
   'In addition to the above date functions, subtract the following amount of days from the calculated date as a buffer.' => 'Der folgende Puffer in Tagen wird von den beiden obigen vorausberechneten Daten abgezogen.',
   'In order to do that hit the button "Delete transaction".' => 'Drücken Sie dafür auf den Button "Buchung löschen".',
   'In order to migrate the old folder structure into the new structure you have to chose which client the old structure will be assigned to.' => 'Um die alte Ordnerstruktur in die neue Struktur zu migrieren, müssen Sie festlegen, welchem Mandanten die bisherige Struktur zugewiesen wird.',
@@ -1875,6 +1880,7 @@ $self->{texts} = {
   'Loading...'                  => 'Wird geladen...',
   'Local Bank Code'             => 'Lokale Bankleitzahl',
   'Local Tax Office Preferences' => 'Angaben zum Finanzamt',
+  'Local account'               => 'Eigenes Konto',
   'Local account number'        => 'Lokale Kontonummer',
   'Local bank account'          => 'Lokales Bankkonto',
   'Local bank code'             => 'Lokale Bankleitzahl',
@@ -1903,6 +1909,8 @@ $self->{texts} = {
   'MD'                          => 'PT',
   'MIME type'                   => 'MIME-Typ',
   'MT940 import'                => 'MT940 Import',
+  'MT940 import preview'        => 'MT940-Import-Vorschau',
+  'MT940 import result'         => 'MT940-Import-Ergebnis',
   'Mails'                       => 'E-Mails',
   'Main Contact Person'         => 'Hauptansprechpartner',
   'Main Preferences'            => 'Grundeinstellungen',
@@ -2044,6 +2052,7 @@ $self->{texts} = {
   'No assembly has been selected yet.' => 'Es wurde noch kein Erzeugnis ausgewahlt.',
   'No background job has been created yet.' => 'Es wurden noch keine Hintergrund-Jobs angelegt.',
   'No bank account chosen!'     => 'Kein Bankkonto ausgewählt!',
+  'No bank account configured for bank code/BIC #1, account number/IBAN #2.' => 'Kein Bankkonto für BLZ/BIC #1, Kontonummer/IBAN #2 konfiguriert.',
   'No bank account flagged for ZUGFeRD usage was found.' => 'Es wurde kein Bankkonto gefunden, das für Nutzung mit ZUGFeRD markiert ist.',
   'No bank information has been entered in this customer\'s master data entry. You cannot create bank collections unless you enter bank information.' => 'Für diesen Kunden wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.',
   'No bank information has been entered in this vendor\'s master data entry. You cannot create bank transfers unless you enter bank information.' => 'Für diesen Lieferanten wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.',
@@ -2066,6 +2075,7 @@ $self->{texts} = {
   'No email for user with login #1 defined.' => 'Keine E-Mail-Adresse für den Benutzer mit dem Login #1 definiert.',
   'No email recipient for customer #1 defined.' => 'Keine E-Mail-Adresse (Rechnungs- oder global) für den Kunden #1 definiert.',
   'No end date given, setting to today' => 'Kein Enddatum gegeben, setze Enddatum auf heute',
+  'No entries can be imported.' => 'Es können keine Einträge importiert werden.',
   'No entries have been imported yet.' => 'Es wurden noch keine Einträge importiert.',
   'No entries have been selected.' => 'Es wurden keine Einträge ausgewählt.',
   'No errors have occurred.'    => 'Es sind keine Fehler aufgetreten.',
@@ -2236,6 +2246,7 @@ $self->{texts} = {
   'Orphaned'                    => 'Nie benutzt',
   'Orphaned currencies'         => 'Verwaiste Währungen',
   'Other Matches'               => 'Andere Treffer',
+  'Other party'                 => 'Andere Partei',
   'Other recipients'            => 'Weitere EmpfängerInnen',
   'Other users\' follow-ups'    => 'Wiedervorlagen anderer Benutzer',
   'Other values are ignored.'   => 'Andere Eingaben werden ignoriert.',
@@ -2364,7 +2375,6 @@ $self->{texts} = {
   'Please contact your administrator or a service provider.' => 'Bitte kontaktieren Sie Ihren Administrator oder einen Dienstleister.',
   'Please contact your administrator.' => 'Bitte wenden Sie sich an Ihren Administrator.',
   'Please correct the settings and try again or deactivate that client.' => 'Bitte korrigieren Sie die Einstellungen und versuchen Sie es erneut, oder deaktivieren Sie diesen Mandanten.',
-  'Please create a CSV import profile called "MT940" for the import type bank transactions:' => 'Bitte erstellen Sie ein CSV Import Profil mit dem Namen "MT940" für den Importtyp Bankbewegungen',
   'Please define a taxkey for the following taxes and run the update again:' => 'Bitte definieren Sie einen Steuerschlüssel für die folgenden Steuern und starten Sie dann das Update erneut:',
   'Please do so in the administration area.' => 'Bitte erledigen Sie dies im Administrationsbereich.',
   'Please enter a profile name.' => 'Bitte geben Sie einen Profilnamen an.',
@@ -2645,6 +2655,7 @@ $self->{texts} = {
   'Remittance information prefix' => 'Verwendungszweckvorbelegung (Präfix)',
   'Remote Bank Code'            => 'Fremde Bankleitzahl',
   'Remote Name/Customer/Description' => 'Kunden/Lieferantenname und Beschreibung',
+  'Remote account'              => 'Gegenkonto',
   'Remote account number'       => 'Fremde Kontonummer',
   'Remote bank code'            => 'Fremde Bankleitzahl',
   'Remote name'                 => 'Fremder Kontoinhaber',
@@ -3227,7 +3238,6 @@ $self->{texts} = {
   'The IBAN is missing.'        => 'Die IBAN fehlt.',
   'The ID #1 is not a valid database ID.' => 'Die ID #1 ist keine gültige Datenbank-ID.',
   'The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.' => 'Der LDAP-Server "#1:#2" ist nicht erreichbar. Bitte überprüfen Sie die Angaben in config/kivitendo.conf.',
-  'The MT940 import needs an import profile called MT940' => 'Der MT940 Import benötigt ein Importprofil mit dem Namen "MT940"',
   'The Mail strings have been saved.' => 'Die vorbelegten E-Mail-Texte wurden gespeichert.',
   'The PDF has been created'    => 'Die PDF-Datei wurde erstellt.',
   'The PDF has been printed'    => 'Das PDF-Dokument wurde gedruckt.',
@@ -3689,6 +3699,7 @@ $self->{texts} = {
   'To (time)'                   => 'Bis',
   'To Date'                     => 'Bis',
   'To continue please change the taxkey 0 to another value.' => 'Um fortzufahren, ändern Sie bitte den Steuerschlüssel 0 auf einen anderen Wert.',
+  'To import'                   => 'Zu importieren',
   'To upload images: Please create shoppart first' => 'Um Bilder hochzuladen bitte Shopartikel zuerst anlegen',
   'To user login'               => 'Zum Benutzerlogin',
   'Toggle marker'               => 'Markierung umschalten',
@@ -3701,6 +3712,7 @@ $self->{texts} = {
   'Total'                       => 'Summe',
   'Total Fees'                  => 'Kumulierte Gebühren',
   'Total Sales Orders Value'    => 'Auftragseingang',
+  'Total number of entries'     => 'Gesamtzahl Einträge',
   'Total stock value'           => 'Gesamter Bestandswert',
   'Total sum'                   => 'Gesamtsumme',
   'Total weight'                => 'Gesamtgewicht',
@@ -3714,6 +3726,7 @@ $self->{texts} = {
   'Transaction ID missing.'     => 'Die Buchungs-ID fehlt.',
   'Transaction Value'           => 'Umsatz',
   'Transaction Value Currency Code' => 'WKZ Umsatz',
+  'Transaction date'            => 'Buchungsdatum',
   'Transaction deleted!'        => 'Buchung gelöscht!',
   'Transaction description'     => 'Vorgangsbezeichnung',
   'Transaction has already been cancelled!' => 'Diese Buchung wurde bereits storniert.',
@@ -4043,6 +4056,7 @@ $self->{texts} = {
   'You have to grant users access to one or more clients.' => 'Benutzern muss dann Zugriff auf einzelne Mandanten gewährt werden.',
   'You have to specify a department.' => 'Sie müssen eine Abteilung wählen.',
   'You have to specify an execution date for each antry.' => 'Sie müssen für jeden zu buchenden Eintrag ein Ausführungsdatum angeben.',
+  'You have to upload an MT940 file to import.' => 'Sie müssen die zu importierende MT940-Datei hochladen.',
   'You must chose a user.'      => 'Sie müssen einen Benutzer auswählen.',
   'You must enter a name for your new print templates.' => 'Sie müssen einen Namen für die neuen Druckvorlagen angeben.',
   'You must not change this AP transaction.' => 'Sie dürfen diese Kreditorenbuchung nicht verändern.',
diff --git a/templates/webpages/bank_import/import_mt940.html b/templates/webpages/bank_import/import_mt940.html
new file mode 100644 (file)
index 0000000..b08eaa8
--- /dev/null
@@ -0,0 +1,93 @@
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE T8 %]
+
+<h1>[% FORM.title %]</h1>
+
+[% IF preview %]
+  <form method="post" action="controller.pl" enctype="multipart/form-data" id="form">
+    [% L.hidden_tag('file_name', SELF.file_name) %]
+  </form>
+[% END %]
+
+<h2>[% LxERP.t8("Overview") %]</h2>
+
+<div>
+  <table>
+    <tr>
+      <td>[% LxERP.t8("Total number of entries") %]:</td>
+      <td align="right">[% SELF.statistics.total %]</td>
+    </tr>
+
+    <tr>
+      <td>[% LxERP.t8("Entries with errors") %]:</td>
+      <td align="right">[% SELF.statistics.errors %]</td>
+    </tr>
+
+    <tr>
+      <td>[% LxERP.t8("Already imported entries (duplicates)") %]:</td>
+      <td align="right">[% SELF.statistics.duplicates %]</td>
+    </tr>
+
+    <tr>
+[% IF preview %]
+      <td>[% LxERP.t8("Entries ready to import") %]:</td>
+      <td align="right">[% SELF.statistics.to_import %]</td>
+[% ELSE %]
+      <td>[% LxERP.t8("Imported entries") %]:</td>
+      <td align="right">[% SELF.statistics.imported %]</td>
+[% END %]
+    </tr>
+  </table>
+</div>
+
+[% IF SELF.statistics.total %]
+
+<h2>[% LxERP.t8("Transactions") %]</h2>
+
+<table>
+  <thead>
+    <tr class="listheading">
+      <th>[% LxERP.t8("Transaction date") %]</th>
+      <th>[% LxERP.t8("Valutadate") %]</th>
+      <th>[% LxERP.t8("Other party") %]</th>
+      <th>[% LxERP.t8("Purpose") %]</th>
+      <th>[% LxERP.t8("Amount") %]</th>
+      <th>[% LxERP.t8("Remote account") %]</th>
+      <th>[% LxERP.t8("Local account") %]</th>
+      <th>[% LxERP.t8("Information") %]</th>
+    </tr>
+  </thead>
+
+  <tbody>
+    [% FOREACH transaction = SELF.transactions %]
+      <tr class="listrow[% IF transaction.error %]_error[% END %]">
+        <td align="right">[% transaction.transdate.to_kivitendo %]</td>
+        <td align="right">[% transaction.valutadate.to_kivitendo %]</td>
+        <td>[% HTML.escape(transaction.remote_name) %]</td>
+        <td>[% HTML.escape(transaction.purpose) %]</td>
+        <td align="right">[% LxERP.format_amount(transaction.amount, 2) %]</td>
+        <td>
+          [% IF transaction.remote_bank_code && transaction.remote_account_number %]
+            [% HTML.escape(transaction.remote_bank_code) %] / [% HTML.escape(transaction.remote_account_number) %]
+          [% END %]
+        </td>
+        <td>[% HTML.escape(transaction.bank_account.name) %]</td>
+        <td>
+          [% IF transaction.error %]
+            [% HTML.escape(transaction.error) %]
+          [% ELSIF transaction.duplicate %]
+            [% LxERP.t8("Duplicate") %]
+          [% ELSIF preview %]
+            [% LxERP.t8("To import") %]
+          [% ELSE %]
+            [% LxERP.t8("Imported") %]
+          [% END %]
+        </td>
+      </tr>
+    [% END %]
+  </tbody>
+</table>
+
+[% END %]
diff --git a/templates/webpages/bank_import/upload_mt940.html b/templates/webpages/bank_import/upload_mt940.html
new file mode 100644 (file)
index 0000000..b451856
--- /dev/null
@@ -0,0 +1,16 @@
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE T8 %]
+
+[%- INCLUDE 'common/flash.html' %]
+
+<h1>[% FORM.title %]</h1>
+
+<p>
+  [% "Import a MT940 file:" | $T8 %]
+</p>
+
+<form method="post" action="controller.pl" enctype="multipart/form-data" id="form">
+  [% L.input_tag('file', '', type => 'file', accept => '*') %]
+</form>
diff --git a/templates/webpages/bankimport/form.html b/templates/webpages/bankimport/form.html
deleted file mode 100644 (file)
index f9c3473..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-[%- USE HTML %]
-[%- USE LxERP %]
-[%- USE L %]
-[%- USE T8 %]
-
- <div class="listtop">[% FORM.title %]</div>
-
- [% IF profile %]
- <p>
- [% "Import a MT940 file:" | $T8 %]
- </p>
-
- <form method="post" action="controller.pl" enctype="multipart/form-data" id="form">
-    [% L.input_tag('file', '', type => 'file', accept => '*') %]
- </form>
- [% ELSE %]
- <p>
- [% "Please create a CSV import profile called \"MT940\" for the import type bank transactions:" | $T8 %] <a href="[% SELF.url_for(controller => 'CsvImport', action => 'new', 'profile.type' => 'bank_transactions' ) %]">CsvImport</a>
- </p>
- [% END %]