Kunden-/Lieferantenstammdatenimport: zumindest das Testen funktioniert
authorMoritz Bunkus <m.bunkus@linet-services.de>
Tue, 1 Mar 2011 10:18:58 +0000 (11:18 +0100)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Thu, 16 Jun 2011 06:44:20 +0000 (08:44 +0200)
13 files changed:
SL/Controller/CsvImport.pm
SL/Controller/CsvImport/Base.pm [new file with mode: 0644]
SL/Controller/CsvImport/CustomerVendor.pm [new file with mode: 0644]
SL/DB/Customer.pm
SL/DB/Vendor.pm
locale/de/all
templates/webpages/csv_import/_data.html [new file with mode: 0644]
templates/webpages/csv_import/_errors.html [new file with mode: 0644]
templates/webpages/csv_import/_form_customers_vendors.html
templates/webpages/csv_import/_form_parts.html
templates/webpages/csv_import/_preview.html [new file with mode: 0644]
templates/webpages/csv_import/_result.html [new file with mode: 0644]
templates/webpages/csv_import/form.html

index 03b9b40..37d801a 100644 (file)
@@ -6,6 +6,7 @@ use SL::DB::Buchungsgruppe;
 use SL::DB::CsvImportProfile;
 use SL::Helper::Flash;
 use SL::SessionFile;
+use SL::Controller::CsvImport::CustomerVendor;
 
 use List::MoreUtils qw(none);
 
@@ -13,7 +14,8 @@ use parent qw(SL::Controller::Base);
 
 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) ],
+ 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
+                import_status errors headers data num_imported) ],
 );
 
 __PACKAGE__->run_before('check_auth');
@@ -128,18 +130,25 @@ sub test_and_import {
   $self->profile_from_form;
 
   if ($::form->{file}) {
-    my $file = SL::SessionFile->new($self->csv_file_name, "w");
+    my $file = SL::SessionFile->new($self->csv_file_name, mode => '>');
     $file->fh->print($::form->{file});
     $file->fh->close;
   }
 
-  my $file = SL::SessionFile->new($self->csv_file_name, "w");
+  my $file = SL::SessionFile->new($self->csv_file_name, mode => '<', encoding => $self->profile->get('charset'));
   if (!$file->fh) {
     flash('error', $::locale->text('No file has been uploaded yet.'));
     return $self->action_new;
   }
 
-  # do the import thingy...
+  my $worker = $self->{type} eq 'customers_vendors' ? SL::Controller::CsvImport::CustomerVendor->new(controller => $self, file => $file)
+    :                                                 die "Program logic error";
+
+  $worker->run;
+  $worker->save_objects if !$params{test};
+
+  $self->import_status($params{test} ? 'tested' : 'imported');
+
   $self->action_new;
 }
 
diff --git a/SL/Controller/CsvImport/Base.pm b/SL/Controller/CsvImport/Base.pm
new file mode 100644 (file)
index 0000000..13ae97c
--- /dev/null
@@ -0,0 +1,102 @@
+package SL::Controller::CsvImport::Base;
+
+use strict;
+
+use SL::Helper::Csv;
+
+use parent qw(Rose::Object);
+
+use Rose::Object::MakeMethods::Generic
+(
+ scalar                  => [ qw(controller file csv) ],
+ 'scalar --get_set_init' => [ qw(profile existing_objects class manager_class) ],
+);
+
+sub run {
+  my ($self) = @_;
+
+  $::lxdebug->dump(0, "file", $self->file);
+  $::lxdebug->dump(0, "profile", $self->controller->profile);
+  my $profile = $self->profile;
+  $self->csv(SL::Helper::Csv->new(file                   => $self->file->file_name,
+                                  encoding               => $self->controller->profile->get('charset'),
+                                  class                  => $self->class,
+                                  profile                => $profile,
+                                  ignore_unknown_columns => 1,
+                                  map { ( $_ => $self->controller->profile->get($_) ) } qw(sep_char escape_char quote_char),
+                                 ));
+  $self->csv->parse;
+
+  $self->controller->errors([ $self->csv->errors ]) if $self->csv->errors;
+
+  $::lxdebug->dump(0, "err", $self->csv->errors);
+
+  return unless $self->csv->header;
+
+  my $headers         = { headers => [ grep { $profile->{$_} } @{ $self->csv->header } ] };
+  $headers->{methods} = [ map { $profile->{$_} } @{ $headers->{headers} } ];
+  $self->controller->headers($headers);
+
+  $self->controller->data([ map { { object => $_, errors => [] } } $self->csv->get_objects ]);
+
+  $self->check_objects;
+  $self->check_duplicates if $self->controller->profile->get('duplicates', 'no_check') ne 'no_check';
+}
+
+sub init_profile {
+  my ($self) = @_;
+
+  eval "require " . $self->class;
+
+  my %profile;
+  for my $col ($self->class->meta->columns) {
+    my $name = $col->isa('Rose::DB::Object::Metadata::Column::Numeric')   ? "$col\_as_number"
+      :        $col->isa('Rose::DB::Object::Metadata::Column::Date')      ? "$col\_as_date"
+      :        $col->isa('Rose::DB::Object::Metadata::Column::Timestamp') ? "$col\_as_date"
+      :                                                                     $col->name;
+
+    $profile{$col} = $name;
+  }
+
+  $self->profile(\%profile);
+}
+
+sub init_existing_objects {
+  my ($self) = @_;
+
+  eval "require " . $self->class;
+  $self->existing_objects($self->manager_class->get_all);
+}
+
+sub init_class {
+  die "class not set";
+}
+
+sub init_manager_class {
+  my ($self) = @_;
+
+  $self->class =~ m/^SL::DB::(.+)/;
+  $self->manager_class("SL::DB::Manager::" . $1);
+}
+
+sub check_objects {
+}
+
+sub check_duplicates {
+}
+
+sub save_objects {
+  my ($self, %params) = @_;
+
+  my $data = $params{data} || $self->controller->data;
+
+  foreach my $entry (@{ $data }) {
+    if (!$entry->{object}->save) {
+      push @{ $entry->{errors} }, $::locale->text('Error when saving: #1', $entry->{object}->db->error);
+    } else {
+      $self->controller->num_imported($self->controller->num_imported + 1);
+    }
+  }
+}
+
+1;
diff --git a/SL/Controller/CsvImport/CustomerVendor.pm b/SL/Controller/CsvImport/CustomerVendor.pm
new file mode 100644 (file)
index 0000000..5fe15af
--- /dev/null
@@ -0,0 +1,84 @@
+package SL::Controller::CsvImport::CustomerVendor;
+
+use strict;
+
+use SL::Helper::Csv;
+
+use parent qw(SL::Controller::CsvImport::Base);
+
+use Rose::Object::MakeMethods::Generic
+(
+ scalar => [ qw(table) ],
+);
+
+sub run {
+  my ($self) = @_;
+
+  $self->table($self->controller->profile->get('table') eq 'customer' ? 'customer' : 'vendor');
+  $self->class('SL::DB::' . ucfirst($self->table));
+
+  $self->SUPER::run;
+}
+
+sub check_objects {
+  my ($self) = @_;
+
+  my $numbercolumn  = $self->controller->profile->get('table') . "number";
+  my %vcs_by_number = map { ( $_->$numbercolumn => 1 ) } @{ $self->existing_objects };
+
+  foreach my $entry (@{ $self->controller->data }) {
+    my $object = $entry->{object};
+
+    my $name =  $object->name;
+    $name    =~ s/^\s+//;
+    $name    =~ s/\s+$//;
+    if (!$name) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Name missing');
+      next;
+    }
+
+    if ($vcs_by_number{ $object->$numbercolumn }) {
+      $entry->{object}->$numbercolumn('####');
+    } else {
+      $vcs_by_number{ $object->$numbercolumn } = $object;
+    }
+  }
+}
+
+sub check_duplicates {
+  my ($self, %params) = @_;
+
+  my $normalizer = sub { my $name = $_[0]; $name =~ s/[\s,\.\-]//g; return $name; };
+
+  my %by_name;
+  if ('check_db' eq $self->controller->profile->get('duplicates')) {
+    %by_name = map { ( $normalizer->($_->name) => 1 ) } @{ $self->existing_objects };
+  }
+
+  foreach my $entry (@{ $self->controller->data }) {
+    next if @{ $entry->{errors} };
+
+    my $name = $normalizer->($entry->{object}->name);
+    if (!$by_name{$name}) {
+      $by_name{$name} = 1;
+
+    } else {
+      push @{ $entry->{errors} }, $::locale->text('Duplicate');
+    }
+  }
+}
+
+sub save_objects {
+  my ($self, %params) = @_;
+
+  my $numbercolumn   = $self->table . 'number';
+  my $with_number    = [ grep { $_->{object}->$numbercolumn ne '####' } @{ $self->controller->data } ];
+  my $without_number = [ grep { $_->{object}->$numbercolumn eq '####' } @{ $self->controller->data } ];
+
+  map { $_->{object}->$numbercolumn('') } @{ $without_number };
+
+  $self->SUPER::save_objects(data => $with_number);
+  $self->SUPER::save_objects(data => $without_number);
+}
+
+1;
index 55a1ee4..85d6999 100644 (file)
@@ -32,6 +32,7 @@ sub _before_save_set_customernumber {
   my ($self) = @_;
 
   $self->create_trans_number if $self->customernumber eq '';
+  return 1;
 }
 
 sub short_address {
index e8ec8bc..d1d972a 100644 (file)
@@ -31,6 +31,7 @@ sub _before_save_set_vendornumber {
   my ($self) = @_;
 
   $self->create_trans_number if $self->vendornumber eq '';
+  return 1;
 }
 
 1;
index a542bac..9d54cbb 100644 (file)
@@ -182,11 +182,9 @@ $self->{texts} = {
   'Ap aging on %s'              => 'Offene Verbindlichkeiten zum %s',
   'Application Error. No Format given' => 'Fehler in der Anwendung. Das Ausgabeformat fehlt.',
   'Application Error. Wrong Format' => 'Fehler in der Anwendung. Falsches Format: ',
-  'Applying #1:'                => 'Führe #1 aus:',
-  'Approximately #1 prices will be updated.' => 'Ungefähr #1 Preise werden aktualisiert.',
-  'Apply Filter'                => 'Filtern',
   'Apply to all parts'          => 'Bei allen Artikeln setzen',
   'Apply to parts without buchungsgruppe' => 'Bei allen Artikeln ohne gültige Buchungsgruppe setzen',
+  'Applying #1:'                => 'Führe #1 aus:',
   'Approximately #1 prices will be updated.' => 'Ungefähr #1 Preise werden aktualisiert.',
   'Apr'                         => 'Apr',
   'April'                       => 'April',
@@ -281,6 +279,7 @@ $self->{texts} = {
   'Birthday'                    => 'Geburtstag',
   'Bis'                         => 'bis',
   'Bis Konto: '                 => 'bis Konto: ',
+  'Block'                       => 'Block',
   'Body'                        => 'Text',
   'Body:'                       => 'Text:',
   'Books are open'              => 'Die Bücher sind geöffnet.',
@@ -371,7 +370,6 @@ $self->{texts} = {
   'Chartaccounts connected to this Tax:' => 'Konten, die mit dieser Steuer verknüpft sind:',
   'Check'                       => 'Scheck',
   'Check Details'               => 'Bitte Angaben überprüfen',
-  'Check the number format.'    => 'Prüfen Sie das Format der Zahlen.',
   'Check for duplicates'        => 'Dublettencheck',
   'Checks'                      => 'Schecks',
   'Choose Customer'             => 'Endkunde wählen:',
@@ -388,8 +386,6 @@ $self->{texts} = {
   'Close Window'                => 'Fenster Schlie&szlig;en',
   'Closed'                      => 'Geschlossen',
   'Collective Orders only work for orders from one customer!' => 'Sammelaufträge funktionieren nur für Aufträge von einem Kunden!',
-  'Column'                      => 'Spalte',
-  'Column definitions can be found here:' => 'Die einzelnen Spaltendefinitionen sind hier verzeichnet:',
   'Comma'                       => 'Komma',
   'Comment'                     => 'Kommentar',
   'Company'                     => 'Firma',
@@ -548,8 +544,8 @@ $self->{texts} = {
   'Delete delivery order'       => 'Lieferschein l&ouml;schen',
   'Delete drafts'               => 'Entwürfe löschen',
   'Delete group'                => 'Gruppe l&ouml;schen',
-  'Delete transaction'          => 'Buchung löschen',
   'Delete profile'              => 'Profil löschen',
+  'Delete transaction'          => 'Buchung löschen',
   'Delivered'                   => 'Geliefert',
   'Delivery Date'               => 'Lieferdatum',
   'Delivery Order'              => 'Lieferschein',
@@ -579,11 +575,13 @@ $self->{texts} = {
   'Dimension unit'              => 'Ma&szlig;einheit',
   'Directory'                   => 'Verzeichnis',
   'Discard duplicate entries in CSV file' => 'Doppelte Einträge in CSV-Datei verwerfen',
-  'Discard entries with duplicates in database and CSV file' => 'Einträge aus CSV-Datei verwerfen, die es bereits in der Datenbank gibt',
+  'Discard entries with duplicates in database or CSV file' => 'Einträge aus CSV-Datei verwerfen, die es bereits in der Datenbank oder der CSV-Datei gibt',
   'Discount'                    => 'Rabatt',
   'Display'                     => 'Anzeigen',
   'Display file'                => 'Datei anzeigen',
   'Display options'             => 'Anzeigeoptionen',
+  'Do not check for duplicates' => 'Nicht nach Dubletten suchen',
+  'Do not set default buchungsgruppe' => 'Nie Standardbuchungsgruppe setzen',
   'Do you really want to close the following SEPA exports? No payment will be recorded for bank collections that haven\'t been marked as executed yet.' => 'Wollen Sie wirklich die folgenden SEPA-Exporte abschließen? Für Überweisungen, die noch nicht gebucht wurden, werden dann keine Zahlungen verbucht.',
   'Do you really want to close the following SEPA exports? No payment will be recorded for bank transfers that haven\'t been marked as executed yet.' => 'Wollen Sie wirklich die folgenden SEPA-Exporte abschließen? Für Überweisungen, die noch nicht gebucht wurden, werden dann keine Zahlungen verbucht.',
   'Do you really want to delete AP transaction #1?' => 'Wollen Sie wirklich die Kreditorenbuchung #1 löschen?',
@@ -591,9 +589,6 @@ $self->{texts} = {
   'Do you really want to delete GL transaction #1?' => 'Wollen Sie wirklich die Dialogbuchung #1 löschen?',
   'Do you really want to delete this group?' => 'Gruppe wirklich l&ouml;schen?',
   'Do you really want to delete this object?' => 'Wollen Sie dieses Objekt wirklich löschen?',
-  'Do not check for duplicates' => 'Nicht nach Dubletten suchen',
-  'Do not set default buchungsgruppe' => 'Nie Standardbuchungsgruppe setzen',
-  'Do you really want to delete this record?' => 'Wollen Sie diesen Beleg wirklich löschen?',
   'Do you really want to delete this warehouse?' => 'Wollen Sie dieses Lager wirklich l&ouml;schen?',
   'Do you want Lx-Office to create a group for access to all functions?' => 'Wollen Sie, dass Lx-Office eine Gruppe mit Zugriff auf alle Funktionen anlegt?',
   'Do you want to <b>limit</b> your search?' => 'Wollen Sie Ihre Suche <b>spezialisieren</b>?',
@@ -627,6 +622,7 @@ $self->{texts} = {
   'Dunning number'              => 'Mahnungsnummer',
   'Dunning overview'            => 'Mahnungsübersicht',
   'Dunnings'                    => 'Mahnungen',
+  'Duplicate'                   => 'Duplikat',
   'During this user migration Lx-Office can create such a group for you and grant all users access to all of Lx-Office\'s functions.' => 'Im Rahmen dieser Benutzerdatenmigration kann Lx-Office eine solche Gruppe f&uuml;r Sie anlegen und allen Benutzern Zugriff auf alle Lx-Office-Funktionen gew&auml;hren.',
   'E-mail'                      => 'eMail',
   'E-mail Statement to'         => 'Fälligkeitsabrechnung als eMail an',
@@ -728,7 +724,10 @@ $self->{texts} = {
   'Error in position #1: You must either assign no transfer at all or the full quantity of #2 #3.' => 'Fehler in Position #1: Sie m&uuml;ssen einer Position entweder gar keinen Lagerausgang oder die vollst&auml;ndige im Lieferschein vermerkte Menge von #2 #3 zuweisen.',
   'Error in row #1: The quantity you entered is bigger than the stocked quantity.' => 'Fehler in Zeile #1: Die angegebene Menge ist gr&ouml;&szlig;er als die vorhandene Menge.',
   'Error message from the database driver:' => 'Fehlermeldung des Datenbanktreibers:',
+  'Error when saving: #1'       => 'Fehler beim Speichern: #2',
   'Error!'                      => 'Fehler!',
+  'Error: Name missing'         => 'Fehler: Name fehlt',
+  'Errors'                      => 'Fehler',
   'Ertrag'                      => 'Ertrag',
   'Ertrag prozentual'           => 'Ertrag prozentual',
   'Escape character'            => 'Escape-Zeichen',
@@ -806,6 +805,8 @@ $self->{texts} = {
   'Foreign Revenues'            => 'Erl&ouml;se Ausland',
   'Form details (second row)'   => 'Formulardetails (zweite Positionszeile)',
   'Formula'                     => 'Formel',
+  'Found #1 errors.'            => '#1 Fehler gefunden.',
+  'Found #1 objects to import.' => '#1 zu importierende Objekte gefunden.',
   'Free report period'          => 'Freier Zeitraum',
   'Free-form text'              => 'Textzeile',
   'Fristsetzung'                => 'Fristsetzung',
@@ -826,7 +827,6 @@ $self->{texts} = {
   'Given Name'                  => 'Vorname',
   'Go one step back'            => 'Einen Schritt zur&uuml;ck',
   'Go one step forward'         => 'Einen Schritt vorw&auml;rts',
-  'Gogogo'                      => '',
   'Greeting'                    => 'Anrede',
   'Greetings'                   => 'Anreden',
   'Group'                       => 'Warengruppe',
@@ -871,10 +871,12 @@ $self->{texts} = {
   'If you want to set up the authentication database yourself then log in to the administration panel. Lx-Office will then create the database and tables for you.' => 'Wenn Sie die Authentifizierungsdatenbank selber einrichten wollen, so melden Sie sich an der Administrationsoberfl&auml;che an. Lx-Office wird dann die Datenbank und die Tabellen f&uuml;r Sie anlegen.',
   'If you yourself want to upgrade the installation then please read the file &quot;doc/UPGRADE&quot; and follow the steps outlined in this file.' => 'Wenn Sie selber die Aktualisierung bzw. Einrichtung &uuml;bernehmen wollen, so lesen Sie bitte die Datei &quot;doc/UPGRADE&quot; und folgen Sie den dort beschriebenen Schritten.',
   'Image'                       => 'Grafik',
+  'Import'                      => 'Import',
   'Import CSV'                  => 'CSV-Import',
-  'Import Help'                 => 'Importhilfe',
   'Import file'                 => 'Import-Datei',
+  'Import preview'              => 'Import-Vorschau',
   'Import profiles'             => 'Import-Profil',
+  'Import result'               => 'Import-Ergebnis',
   'In Lx-Office 2.4.0 the administrator has to enter a list of units in the administrative section.' => 'In Lx-Office 2.4.0 muss der Administrator in den Systemeinstellungen eine Liste von verwendbaren Einheiten angeben.',
   'In order to do that hit the button "Delete transaction".' => 'Drücken Sie dafür auf den Button "Buchung löschen".',
   'In the latter case the tables needed by Lx-Office will be created in that database.' => 'In letzterem Fall werden die von Lx-Office benötigten Tabellen in dieser existierenden Datenbank angelegt.',
@@ -990,6 +992,7 @@ $self->{texts} = {
   'Licenses'                    => 'Lizenzen',
   'Limit part selection'        => 'Artikelauswahl eingrenzen',
   'Line Total'                  => 'Zeilensumme',
+  'Line and column'             => 'Zeile und Spalte',
   'Line endings'                => 'Zeilenumbr&uuml;che',
   'List'                        => 'Anzeigen',
   'List Accounting Groups'      => 'Buchungsgruppen anzeigen',
@@ -1233,7 +1236,6 @@ $self->{texts} = {
   'Part Notes'                  => 'Bemerkungen',
   'Part Number'                 => 'Artikelnummer',
   'Part Number missing!'        => 'Artikelnummer fehlt!',
-  'Partnumber'                  => '',
   'Partnumber must not be set to empty!' => 'Die Artikelnummer darf nicht auf leer ge&auml;ndert werden.',
   'Partnumber not unique!'      => 'Artikelnummer bereits vorhanden!',
   'Parts'                       => 'Waren',
@@ -1272,8 +1274,8 @@ $self->{texts} = {
   'Please ask your administrator to create warehouses and bins.' => 'Bitten Sie Ihren Administrator, dass er Lager und Lagerpl&auml;tze anlegt.',
   'Please enter a license key.' => 'Bitte geben Sie einen Lizenzschlüssel an.',
   'Please enter a number of licenses.' => 'Bitte geben Sie die Anzahl Lizenzschlüssel an.',
-  'Please enter the login for the new user.' => 'Bitte geben Sie das Login für den neuen Benutzer ein.',
   'Please enter a profile name.' => 'Bitte geben Sie einen Profilnamen an.',
+  'Please enter the login for the new user.' => 'Bitte geben Sie das Login für den neuen Benutzer ein.',
   'Please enter the name of the database that will be used as the template for the new database:' => 'Bitte geben Sie den Namen der Datenbank an, die als Vorlage f&uuml;r die neue Datenbank benutzt wird:',
   'Please enter the name of the dataset you want to restore the backup in.' => 'Bitte geben Sie den Namen der Datenbank ein, in der Sie die Sicherung wiederherstellen wollen.',
   'Please enter the sales tax identification number.' => 'Bitte geben Sie die Umsatzsteueridentifikationsnummer an.',
@@ -1520,12 +1522,9 @@ $self->{texts} = {
   'Selection'                   => 'Auswahlbox',
   'Selection fields: The option field must contain the available options for the selection. Options are separated by \'##\', for example \'Early##Normal##Late\'.' => 'Auswahlboxen: Das Optionenfeld muss die f&uuml;r die Auswahl verf&uuml;gbaren Eintr&auml;ge enthalten. Die Eintr&auml;ge werden mit \'##\' voneinander getrennt. Beispiel: \'Fr&uuml;h##Normal##Sp&auml;t\'.',
   'Sell Price'                  => 'Verkaufspreis',
-  'Sell prices have fallen below the minimum sell price. PDF export and email deactivated until the quotation has been approved.' => 'Verkaufspreise liegen teilweise unter dem Mindestverkaufspreis. PDF-Export und E-Mail deaktiviert, bis das Angebot freigegeben wurde.',
-  'Sellprice'                   => 'Verkaufspreis',
   'Sellprice adjustment'        => 'Verkaufspreis: Preisanpassung',
   'Sellprice significant places' => 'Verkaufspreis: Nachkommastellen',
   'Semicolon'                   => 'Semikolon',
-  'Send documents'              => 'Dokumente verschicken',
   'Send the backup via Email'   => 'Die Sicherungsdatei per Email verschicken',
   'Sep'                         => 'Sep',
   'Separator'                   => 'Trennzeichen',
@@ -1541,9 +1540,9 @@ $self->{texts} = {
   'Services'                    => 'Dienstleistungen',
   'Set Language Values'         => 'Spracheinstellungen',
   'Set eMail text'              => 'eMail Text eingeben',
+  'Settings'                    => 'Einstellungen',
   'Setup Menu'                  => 'Menü-Variante',
   'Setup Templates'             => 'Vorlagen auswählen',
-  'Settings'                    => 'Einstellungen',
   'Ship to'                     => 'Lieferadresse',
   'Ship via'                    => 'Transportmittel',
   'Shipping Address'            => 'Lieferadresse',
@@ -1563,8 +1562,8 @@ $self->{texts} = {
   'Show your TODO list after loggin in' => 'Aufgabenliste nach dem Anmelden anzeigen',
   'Signature'                   => 'Unterschrift',
   'Since bin is not enforced in the parts data, please specify a bin where goods without a specified bin will be put.' => 'Da Lagerpl&auml;tze kein Pflichtfeld sind, geben Sie bitte einen Lagerplatz an, in dem Waren ohne spezifizierten Lagerplatz eingelagert werden sollen.',
-  'Skip'                        => 'Überspringen',
   'Single quotes'               => 'Einfache Anführungszeichen',
+  'Skip'                        => 'Überspringen',
   'Skonto'                      => 'Skonto',
   'Skonto Terms'                => 'Zahlungsziel Skonto',
   'Sold'                        => 'Verkauft',
@@ -1661,6 +1660,7 @@ $self->{texts} = {
   'Template database'           => 'Datenbankvorlage',
   'Templates'                   => 'Vorlagen',
   'Terms missing in row '       => '+Tage fehlen in Zeile ',
+  'Test and preview'            => 'Test und Vorschau',
   'Test connection'             => 'Verbindung testen',
   '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.',
@@ -1923,7 +1923,6 @@ $self->{texts} = {
   'Update prices of existing entries' => 'Preise von vorhandenen Artikeln aktualisieren',
   'Update?'                     => 'Aktualisieren?',
   'Updated'                     => 'Erneuert am',
-  'Use'                         => 'Verwendung',
   'Uploaded on #1, size #2 kB'  => 'Am #1 hochgeladen, Größe #2 kB',
   'Use As Template'             => 'Als Vorlage verwenden',
   'Use Templates'               => 'Benutze Vorlagen',
@@ -2144,8 +2143,8 @@ $self->{texts} = {
   'order'                       => 'Reihenfolge',
   'our vendor number at customer' => 'Unsere Lieferanten-Nr. beim Kunden',
   'part_list'                   => 'warenliste',
-  'pick_list'                   => 'Sammelliste',
   'percental'                   => 'prozentual',
+  'pick_list'                   => 'Sammelliste',
   'plural first char'           => 'P',
   'pos_bilanz'                  => 'Bilanz',
   'pos_bwa'                     => 'BWA',
diff --git a/templates/webpages/csv_import/_data.html b/templates/webpages/csv_import/_data.html
new file mode 100644 (file)
index 0000000..3663382
--- /dev/null
@@ -0,0 +1,12 @@
+[% USE LxERP %]
+
+[%- IF SELF.errors %]
+ [%- PROCESS 'csv_import/_errors.html' %]
+[%- END %]
+
+[%- IF SELF.import_status == 'imported' %]
+ [%- PROCESS 'csv_import/_result.html' %]
+[%- END %]
+
+[%- PROCESS 'csv_import/_preview.html' %]
+
diff --git a/templates/webpages/csv_import/_errors.html b/templates/webpages/csv_import/_errors.html
new file mode 100644 (file)
index 0000000..3fda491
--- /dev/null
@@ -0,0 +1,21 @@
+[% USE LxERP %]
+
+ <h3>[%- LxERP.t8('Errors') %]</h3>
+
+ <p>[%- LxERP.t8('Found #1 errors.', SELF.errors.size) %]</p>
+
+ <table>
+  <tr class="listheading">
+   <th>[%- LxERP.t8('Line and column') %]</th>
+   <th>[%- LxERP.t8('Block') %]</th>
+   <th>[%- LxERP.t8('Error') %]</th>
+  </tr>
+  [% FOREACH err = SELF.errors %]
+   <tr>
+    <td>[% err.4 %]:[% err.3 %]</td>
+    <td>[% err.0 %]</td>
+    <td>[% err.2 %]</td>
+   </tr>
+  [% END %]
+ </table>
+[% END %]
index f53dcf3..c3bf9a5 100644 (file)
@@ -1,3 +1,6 @@
+[% USE LxERP %]
+[% USE L %]
+
 <tr>
  <th align="right">[%- LxERP.t8('Target table') %]:</th>
  <td colspan="10">
index 26465aa..21dd423 100644 (file)
@@ -1,3 +1,5 @@
+[% USE LxERP %]
+[% USE L %]
 <tr>
  <th align="right">[%- LxERP.t8('Parts with existing part numbers') %]:</th>
  <td colspan="10">
diff --git a/templates/webpages/csv_import/_preview.html b/templates/webpages/csv_import/_preview.html
new file mode 100644 (file)
index 0000000..323fdb4
--- /dev/null
@@ -0,0 +1,35 @@
+[% USE HTML %]
+[% USE LxERP %]
+
+<h3>
+ [%- IF SELF.import_status == 'tested' %]
+  [%- LxERP.t8('Import preview') %]
+ [%- ELSE %]
+  [%- LxERP.t8('Import result') %]
+ [%- END %]
+</h3>
+
+<p>[%- LxERP.t8('Found #1 objects to import.', SELF.data.size || 0) %]</p>
+
+[% IF SELF.data.size %]
+ <table>
+  <tr class="listheading">
+   [%- FOREACH column = SELF.headers.headers %]
+    <th>[%- HTML.escape(column) %]</th>
+   [%- END %]
+   <th>[%- LxERP.t8('Notes') %]</th>
+  </tr>
+
+  [%- FOREACH row = SELF.data %]
+  <tr class="[% IF row.errors.size %]redrow[% ELSE %]listrow[% END %][% loop.count % 2 %]">
+   [%- FOREACH method = SELF.headers.methods %]
+    <td>[%- HTML.escape(row.object.$method) %]</td>
+   [%- END %]
+   <td>
+    [%- FOREACH error = row.errors %][%- HTML.escape(error) %][% UNLESS loop.last %]<br>[%- END %][%- END %]
+   </td>
+  </tr>
+  [%- END %]
+
+ </table>
+[%- END %]
diff --git a/templates/webpages/csv_import/_result.html b/templates/webpages/csv_import/_result.html
new file mode 100644 (file)
index 0000000..0896518
--- /dev/null
@@ -0,0 +1,3 @@
+[% USE LxERP %]
+
+<p>[%- LxERP.t8('Found #1 objects to import.', SELF.data.size || 0) %]</p>
index 4ae1cf6..bc23d54 100644 (file)
    <tr>
     <th align="right">[%- LxERP.t8('Check for duplicates') %]:</th>
     <td colspan="10">
-     [% opts = [ [ 'no_check', LxERP.t8('Do not check for duplicates') ],
+     [% opts = [ [ 'no_check',  LxERP.t8('Do not check for duplicates') ],
                  [ 'check_csv', LxERP.t8('Discard duplicate entries in CSV file') ],
-                 [ 'check_db',  LxERP.t8('Discard entries with duplicates in database and CSV file') ] ] %]
+                 [ 'check_db',  LxERP.t8('Discard entries with duplicates in database or CSV file') ] ] %]
      [% L.select_tag('settings.duplicates', L.options_for_select(opts, default => SELF.profile.get('duplicates')), style => 'width: 300px') %]
     </td>
    </tr>
 
   </table>
 
-  [% L.submit_tag('action_test', LxERP.t8('Gogogo')) %]
+  [% L.submit_tag('action_test', LxERP.t8('Test and preview')) %]
+  [% IF SELF.import_status && SELF.data.size %]
+   [% L.submit_tag('action_import', LxERP.t8('Import')) %]
+  [%- END %]
 
  </form>
 
+ [%- IF SELF.import_status %]
+  [%- PROCESS 'csv_import/_data.html' %]
+ [%- END %]
+
  <script type="text/javascript">
   <!--
     $(document).ready(function() {