CsvImport: Spaltenzuordnung für non multiplexed
authorSven Schöling <s.schoeling@linet-services.de>
Thu, 16 Jun 2016 13:24:40 +0000 (15:24 +0200)
committerSven Schöling <s.schoeling@linet-services.de>
Thu, 16 Jun 2016 13:45:02 +0000 (15:45 +0200)
SL/BackgroundJob/CsvImport.pm
SL/Controller/CsvImport.pm
SL/Controller/CsvImport/Base.pm
SL/Helper/Csv.pm
SL/Helper/Csv/Dispatcher.pm
locale/de/all
templates/webpages/csv_import/_mapping_item.html [new file with mode: 0644]
templates/webpages/csv_import/form.html

index 1798d67..96583d7 100644 (file)
@@ -5,6 +5,7 @@ use strict;
 use parent qw(SL::BackgroundJob::Base);
 
 use YAML ();
+use SL::JSON;
 use SL::DB::CsvImportProfile;
 use SL::SessionFile::Random;
 
@@ -60,6 +61,7 @@ sub do_import {
   my $job = $self->{db_obj};
 
   $c->profile($self->profile);
+  $c->mappings(SL::JSON::from_json($self->profile->get('json_mappings'))) if $self->profile->get('json_mappings');
   $c->type($job->data_as_hash->{type});
   $c->{employee_id} = $job->data_as_hash->{employee_id};
 
index a177401..e53917e 100644 (file)
@@ -32,7 +32,7 @@ use Rose::Object::MakeMethods::Generic
 (
  scalar                  => [ qw(type profile file all_profiles all_charsets sep_char all_sep_chars quote_char all_quote_chars escape_char all_escape_chars all_buchungsgruppen all_units
                                  import_status errors headers raw_data_headers info_headers data num_importable displayable_columns file all_taxzones) ],
- 'scalar --get_set_init' => [ qw(worker task_server num_imported) ],
+ 'scalar --get_set_init' => [ qw(worker task_server num_imported mappings) ],
  'array'                 => [
    progress_tracker     => { },
    add_progress_tracker => {  interface => 'add', hash_key => 'progress_tracker' },
@@ -213,6 +213,59 @@ sub action_report {
   $self->render('csv_import/report', { layout => !($params{no_layout} || $::form->{no_layout}) });
 }
 
+sub action_add_empty_mapping_line {
+  my ($self) = @_;
+
+  $self->profile_from_form;
+  $self->setup_help;
+
+  $self->js
+    ->append('#csv_import_mappings', $self->render('csv_import/_mapping_item', { layout => 0, output => 0 }))
+    ->hide('#mapping_empty')
+    ->render;
+}
+
+sub action_add_mapping_from_upload {
+  my ($self) = @_;
+
+  $self->profile_from_form;
+  $self->setup_help;
+
+  my $file = SL::SessionFile->new($self->csv_file_name, mode => '<', encoding => $self->profile->get('charset'));
+  if (!$file->fh) {
+    $self->js
+      ->flash('error', t8('No file has been uploaded yet.'))
+      ->render;
+    return;
+  }
+
+  my $csv = Text::CSV_XS->new({
+    binary      => 1,
+    sep_char    => $self->profile->get('sep_char'),
+    quote_char  => $self->profile->get('quote_char'),
+    escape_char => $self->profile->get('escape_char'),
+  });
+
+  my $header = $csv->getline($file->fh) or do {
+    $self->js
+      ->flash('error', t8('No header found'))
+      ->render;
+    return;
+  };
+
+  for my $field (@$header) {
+    next if $self->mappings_for_profile->{$field};
+    $self->js->append(
+      '#csv_import_mappings',
+      $self->render('csv_import/_mapping_item', { layout => 0, output => 0 }, item => { from => $field }),
+    );
+  }
+
+  $self->js
+    ->hide('#mapping_empty')
+    ->render;
+}
+
 
 #
 # filters
@@ -372,6 +425,7 @@ sub load_default_profile {
   $profile ||= SL::DB::CsvImportProfile->new(type => $self->{type}, login => $::myconfig{login});
 
   $self->profile($profile);
+  $self->mappings(SL::JSON::from_json($self->profile->get('json_mappings'))) if $self->profile->get('json_mappings');
   $self->worker->set_profile_defaults;
   $self->profile->set_defaults;
 }
@@ -416,6 +470,9 @@ sub profile_from_form {
   $self->profile->assign_attributes(%{ $::form->{profile} });
   $self->profile->settings(map({ { key => $_, value => $::form->{settings}->{$_} } } keys %{ $::form->{settings} }),
                            @settings);
+
+  $self->profile->set('json_mappings', JSON::to_json($self->mappings));
+
   $self->profile->set_defaults;
 }
 
@@ -664,4 +721,12 @@ sub check_task_server {
   1;
 }
 
+sub mappings_for_profile {
+  +{ map { $_->{from} => $_->{to} } @{ $_[0]->mappings } }
+}
+
+sub init_mappings {
+  $::form->{mappings} || []
+}
+
 1;
index 8544cf7..6906026 100644 (file)
@@ -34,7 +34,7 @@ sub run {
   my $profile = $self->profile;
   $self->csv(SL::Helper::Csv->new(file                   => $self->file->file_name,
                                   encoding               => $self->controller->profile->get('charset'),
-                                  profile                => [{ profile => $profile, class => $self->class }],
+                                  profile                => [{ profile => $profile, class => $self->class, mapping => $self->controller->mappings_for_profile }],
                                   ignore_unknown_columns => 1,
                                   strict_profile         => 1,
                                   case_insensitive_header => 1,
@@ -54,9 +54,9 @@ sub run {
 
   return if ( !$self->csv->header || $self->csv->errors );
 
-  my $headers         = { headers => [ grep { $profile->{$_} } @{ $self->csv->header } ] };
-  $headers->{methods} = [ map { $profile->{$_} } @{ $headers->{headers} } ];
-  $headers->{used}    = { map { ($_ => 1) }      @{ $headers->{headers} } };
+  my $headers         = { headers => [ map {; $_->{key} } @{ $self->csv->specs->[0] } ] };
+  $headers->{methods} = [ map { $_->{path} } @{ $self->csv->specs->[0] } ];
+  $headers->{used}    = { map { ($_ => 1) }  @{ $headers->{headers} } };
   $self->controller->headers($headers);
   $self->controller->raw_data_headers({ used => { }, headers => [ ] });
   $self->controller->info_headers({ used => { }, headers => [ ] });
index fc9ff20..145b7db 100644 (file)
@@ -341,6 +341,9 @@ sub _push_error {
   $self->_errors(\@new_errors);
 }
 
+sub specs {
+  $_[0]->dispatcher->_specs
+}
 
 1;
 
index a063103..a0f59fc 100644 (file)
@@ -160,7 +160,7 @@ sub _parse_profile {
 sub make_spec {
   my ($self, $col, $path, $cur_class) = @_;
 
-  my $spec = { key => $col, steps => [] };
+  my $spec = { key => $col, path => $path, steps => [] };
 
   return unless $path;
 
index 65daedf..d051a8d 100755 (executable)
@@ -183,7 +183,9 @@ $self->{texts} = {
   'Add bank account'            => 'Bankkonto erfassen',
   'Add booking group'           => 'Buchungsgruppe erfassen',
   'Add custom variable'         => 'Benutzerdefinierte Variable erfassen',
+  'Add empty line (csv_import)' => 'Leere Zeile einfügen',
   'Add function block'          => 'Funktionsblock hinzufügen',
+  'Add headers from last uploaded file (csv_import)' => 'Spalten aus der hochgeladenen Datei einfügen',
   'Add invoices'                => 'Rechnungen hinzufügen',
   'Add link: select records to link with' => 'Verknüpfungen hinzufügen: zu verknüpfende Belege auswählen',
   'Add linked record'           => 'Verknüpften Beleg hinzufügen',
@@ -220,7 +222,6 @@ $self->{texts} = {
   'All clients'                 => 'Alle Mandanten',
   'All general ledger entries'  => 'Alle Hauptbucheinträge',
   'All groups'                  => 'Alle Gruppen',
-  'All linked transactions'     => 'Alle verknüpften Transaktionen',
   'All modules'                 => 'Alle Module',
   'All of the exports you have selected were already closed.' => 'Alle von Ihnen ausgewählten Exporte sind bereits abgeschlossen.',
   'All partsgroups'             => 'Alle Warengruppen',
@@ -858,7 +859,6 @@ $self->{texts} = {
   'Delete Contact'              => 'Ansprechperson löschen',
   'Delete Dataset'              => 'Datenbank löschen',
   'Delete Shipto'               => 'Lieferadresse löschen',
-  'Delete bank transactions'    => '',
   'Delete drafts'               => 'Entwürfe löschen',
   'Delete links'                => 'Verknüpfungen löschen',
   'Delete picture'              => 'Bild löschen',
@@ -1365,7 +1365,6 @@ $self->{texts} = {
   'Help Template Variables'     => 'Hilfe zu Dokumenten-Variablen',
   'Help on column names'        => 'Hilfe zu Spaltennamen',
   'Here'                        => 'Hier',
-  'Here we are!'                => '',
   'Here you only provide the credentials for logging into the database.' => 'Hier geben Sie nur die Logindaten für die Anmeldung an der Datenbank ein.',
   'Here\'s an example command line:' => 'Hier ist eine Kommandozeile, die als Beispiel dient:',
   'Hide Filter'                 => 'Filter verbergen',
@@ -1373,6 +1372,7 @@ $self->{texts} = {
   'Hide chart details'          => 'Konteninformation verstecken',
   'Hide details'                => 'Details verbergen',
   'Hide help text'              => 'Hilfetext verbergen',
+  'Hide mappings (csv_import)'  => 'Spaltenzuordnungen verbergen',
   'Hide settings'               => 'Einstellungen verbergen',
   'Hints'                       => 'Hinweise',
   'History'                     => 'Historie',
@@ -1541,6 +1541,7 @@ $self->{texts} = {
   'KNr. beim Kunden'            => 'KNr. beim Kunden',
   'Keep the project link the way it is.' => 'Die aktuelle Verknüpfung beibehalten.',
   'Keine Suchergebnisse gefunden!' => 'Keine Suchergebnisse gefunden!',
+  'Known Column'                => 'Bekannte Spalte',
   'Konten'                      => 'Konten',
   'L'                           => 'L',
   'LIABILITIES'                 => 'PASSIVA',
@@ -1658,6 +1659,7 @@ $self->{texts} = {
   'Mandatory Departments'       => 'Benutzer muss Abteilungen vergeben',
   'Manually sent E-Mails will have their BCC field appended with this address. Will not trigger for employees without the right to send bcc, and will not apply to mails sent by automated jobs.' => 'Diese Mailadresse wird automatisch in das BCC Feld bei Mailversand kopiert. Hat keine Auswirkungen für Mitarbeiter ohne das Recht BCC zu versenden, und ignoriert wenn Mails automatisch versendet werden.',
   'Map'                         => 'Karte',
+  'Mappings (csv_import)'       => 'Spaltenzuordnungen',
   'Mar'                         => 'März',
   'March'                       => 'März',
   'Margepercent'                => 'Ertrag prozentual',
@@ -1788,6 +1790,7 @@ $self->{texts} = {
   'No file has been uploaded yet.' => 'Es wurde noch keine Datei hochgeladen.',
   'No function blocks have been created yet.' => 'Es wurden noch keine Funktionsblöcke angelegt.',
   'No groups have been created yet.' => 'Es wurden noch keine Gruppen angelegt.',
+  'No header found'             => 'Keine Kopfzeile gefunden',
   'No internal phone extensions have been configured yet.' => 'Es wurden noch keine internen Durchwahlen konfiguriert.',
   'No invoices have been selected.' => 'Es wurden keine Rechnungen ausgewählt.',
   'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => 'Es wurde kein oder ein unbekanntes Authentifizierungsmodul in "config/kivitendo.conf" angegeben.',
@@ -2534,6 +2537,7 @@ $self->{texts} = {
   'Show help text'              => 'Hilfetext anzeigen',
   'Show history'                => 'Verlauf anzeigen',
   'Show items from invoices individually' => 'Artikel aus Rechnungen anzeigen',
+  'Show mappings (csv_import)'  => 'Spaltenzuordnungen anzeigen',
   'Show old dunnings'           => 'Alte Mahnungen anzeigen',
   'Show overdue sales quotations and requests for quotations...' => 'Überfällige Angebote und Preisanfragen anzeigen...',
   'Show parts'                  => 'Artikel anzeigen',
@@ -2705,6 +2709,7 @@ $self->{texts} = {
   'Text blocks front'           => 'Textblöcke vorne',
   'Text field'                  => 'Textfeld',
   'Text field variables: \'WIDTH=w HEIGHT=h\' sets the width and height of the text field. They default to 30 and 5 respectively.' => 'Textfelder: \'WIDTH=w HEIGHT=h\' setzen die Breite und die H&ouml;he des Textfeldes. Wenn nicht anders angegeben, so werden sie 30 Zeichen breit und f&uuml;nf Zeichen hoch dargestellt.',
+  'Text in CSV File'            => 'Spalte in der CSV Datei',
   'Text variables: \'MAXLENGTH=n\' sets the maximum entry length to \'n\'.' => 'Textzeilen: \'MAXLENGTH=n\' setzt eine Maximall&auml;nge von n Zeichen.',
   'Text, text field and number variables: The default value will be used as-is.' => 'Textzeilen, Textfelder und Zahlenvariablen: Der Standardwert wird so wie er ist &uuml;bernommen.',
   'Texts for invoices'          => 'Texte für Rechnungen',
@@ -3041,6 +3046,7 @@ $self->{texts} = {
   'There is not enough available of \'#1\' at warehouse \'#2\', bin \'#3\', #4, #5, for the transfer of #6.' => 'Von \'#1\' ist in Lager \'#2\', Lagerplatz \'#3\', #4, #5, nicht gen&uuml;gend eingelagert, um insgesamt #6 auszulagern.',
   'There is not enough available of \'#1\' at warehouse \'#2\', bin \'#3\', #4, for the transfer of #5.' => 'Von \'#1\' ist in Lager \'#2\', Lagerplatz \'#3\', #4 nicht gen&uuml;gend eingelagert, um insgesamt #5 auszulagern.',
   'There is not enough left of \'#1\' in bin \'#2\' for the removal of #3.' => 'In Lagerplatz \'#2\' ist nicht genug von \'#1\' vorhanden, um #3 zu entnehmen.',
+  'There is nothing here yet (csv_import)' => 'Noch keine Zuordnungen',
   'There is one or more sections for which no part has been assigned yet; therefore creating the new record is not possible yet.' => 'Es gibt einen oder mehrere Abschnitte ohne Artikelzuweisung; daher kann der neue Beleg noch nicht erstellt werden.',
   'There was an error deleting the draft' => 'Beim Löschen des Entwurfs ist ein Fehler aufgetretetn',
   'There was an error executing the background job.' => 'Bei der Ausführung des Hintergrund-Jobs trat ein Fehler auf.',
@@ -3051,6 +3057,7 @@ $self->{texts} = {
   'There you can let kivitendo create the basic tables for you, even in an already existing database.' => 'Dort können Sie kivitendo diese grundlegenden Tabellen erstellen lassen, selbst in einer bereits existierenden Datenbank.',
   'Therefore several settings that had to be made for each user in the past have been consolidated into the client configuration.' => 'Dazu wurden gewisse Einstellungen, die vorher bei jedem Benutzer vorgenommen werden mussten, in die Konfiguration eines Mandanten verschoben.',
   'Therefore the definition of "kg" with the base unit "g" and a factor of 1000 is valid while defining "g" with a base unit of "kg" and a factor of "0.001" is not.' => 'So ist die Definition von "kg" mit der Basiseinheit "g" und dem Faktor 1000 zulässig, die Definition von "g" mit der Basiseinheit "kg" und dem Faktor "0,001" hingegen nicht.',
+  'These mappings can be used to map heading from non standard csv files to known columns. These will also be saved in profiles, so you can save profiles for every source of formats.' => 'Mit diesen Spaltenzuordnungen können die Kopfzeilen aus beliebigen CSV-Dateien verarbeitet werden. Die Zuordnungen werden im Profil mitgespeichert, so dass regelmäßige Quellen behandelt werden können.',
   'These wrong entries cannot be fixed automatically.' => 'Diese Einträge können nicht automatisch bereinigt werden.',
   'They will be updated, new ones for additional parts without a line item added automatically.' => 'Diese Positionen werden automatisch aktualisiert bzw. ergänzt, wenn es noch keine Position zu einem zusätzlichen Artikel gibt.',
   'This Price Rule is no longer valid' => 'Diese Preisregel ist nicht mehr gültig',
@@ -3205,8 +3212,6 @@ $self->{texts} = {
   'Unknown dependency \'%s\'.'  => 'Unbekannte Abh&auml;ngigkeit \'%s\'.',
   'Unknown module: #1'          => 'Unbekanntes Modul #1',
   'Unknown problem type.'       => 'Unbekannter Problem-Typ',
-  'Unlink Bank Transactions'    => 'Bankverknüpfungen aufheben',
-  'Unlink bank transaction'     => 'Bankverknüpfung aufheben',
   'Unlock System'               => 'System entsperren',
   'Unsuccessfully executed:\n'  => 'Erfolglos ausgeführt:',
   'Unsupported image type (supported types: #1)' => 'Nicht unterstützter Bildtyp (unterstützte Typen: #1)',
diff --git a/templates/webpages/csv_import/_mapping_item.html b/templates/webpages/csv_import/_mapping_item.html
new file mode 100644 (file)
index 0000000..9e5f74c
--- /dev/null
@@ -0,0 +1,14 @@
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE T8 %]
+ <tr class=listrow>
+    <td><a class='remove_line interact cursor-pointer'>✘</a></td>
+[%- IF item.from %]
+  <td>[% L.hidden_tag('mappings[+].from', item.from) %][% item.from | html %]</td>
+[%- ELSE %]
+  <td>[% L.input_tag('mappings[+].from', '') %]</td>
+[%- END %]
+  <td>[% L.select_tag('mappings[].to', SELF.displayable_columns, value_key='name', title_key='name', default=item.to) %]</td>
+ </tr>
+
index 792aa20..9b8fd1a 100644 (file)
   </div>
   <hr>
 
+[%- UNLESS SELF.worker.is_multiplexed %]
+  <h2>[% 'Mappings (csv_import)' | $T8 %]</h2>
+
+  <div class="mappings_toggle"[% UNLESS SELF.deferred || SELF.import_status %] style="display:none"[% END %]>
+   <a href="#" onClick="javascript:$('.mappings_toggle').toggle()">[% LxERP.t8("Show mappings (csv_import)") %]</a>
+  </div>
+  <div class="mappings_toggle"[% IF SELF.deferred || SELF.import_status %] style="display:none"[% END %]>
+   <p><a href="#" onClick="javascript:$('.mappings_toggle').toggle()">[% LxERP.t8("Hide mappings (csv_import)") %]</a></p>
+
+    <p>[% 'These mappings can be used to map heading from non standard csv files to known columns. These will also be saved in profiles, so you can save profiles for every source of formats.' | $T8 %]</p>
+
+  <table id="csv_import_mappings">
+   <tr class=listheading>
+    <th></th>
+    <th>[% 'Text in CSV File' | $T8 %]</th>
+    <th>[% 'Known Column' | $T8 %]</th>
+   </tr>
+   <tr id='mapping_empty' style='display:none'>
+    <td colspan=3>[% 'There is nothing here yet (csv_import)' | $T8 %]</td>
+   </tr>
+[%- FOREACH row = SELF.mappings %]
+   [% PROCESS 'csv_import/_mapping_item.html', item=row IF row.from %]
+[%- END %]
+   [% PROCESS 'csv_import/_mapping_item.html', item={} %]
+  </table>
+
+  <input type=button id='add_empty_mapping_line' value='[% 'Add empty line (csv_import)' | $T8 %]'>
+  <input type=button id='add_mapping_from_upload' value='[% 'Add headers from last uploaded file (csv_import)' | $T8 %]'>
+
+  </div>
+  <hr>
+[%- END %]
   [% L.submit_tag('action_test', LxERP.t8('Test and preview')) %]
   [% L.submit_tag('action_import', LxERP.t8('Import'), style='display:none') %]
 
           return true;
         alert('[% LxERP.t8('Please enter a profile name.') %]');
         return false;
-      })
+      });
+      $('#add_empty_mapping_line').click(function(){
+        $.get('controller.pl', { action: 'CsvImport/add_empty_mapping_line', 'profile.type': $('#profile_type').val() }, kivi.eval_json_result);
+      });
+      $('#add_mapping_from_upload').click(function(){
+        $.get('controller.pl?action_add_mapping_from_upload=1', $('form').serialize() , kivi.eval_json_result);
+      });
+      $('#csv_import_mappings').on('click', '.remove_line', function(){ $(this).closest('tr').remove(); if (1==$('#csv_import_mappings tr:visible').length) $('#mapping_empty').show() });
     });
     -->
  </script>