CSV-Import für Lagerbewegung, bzw. Lagerbestand.
authorBernd Bleßmann <bernd@kivitendo-premium.de>
Mon, 10 Feb 2014 09:21:15 +0000 (10:21 +0100)
committerBernd Bleßmann <bernd@kivitendo-premium.de>
Wed, 28 Jan 2015 22:26:39 +0000 (23:26 +0100)
Conflicts:

SL/Controller/CsvImport.pm
locale/de/all
locale/en/all

Übernahme aus Kundenprojekt.

SL/Controller/CsvImport.pm
SL/Controller/CsvImport/Inventory.pm [new file with mode: 0644]
locale/de/all
locale/en/all
menus/erp.ini
templates/webpages/csv_import/_form_inventories.html [new file with mode: 0644]
templates/webpages/csv_import/form.html

index 088e8c3..3cd0da2 100644 (file)
@@ -13,6 +13,7 @@ use SL::SessionFile;
 use SL::Controller::CsvImport::Contact;
 use SL::Controller::CsvImport::CustomerVendor;
 use SL::Controller::CsvImport::Part;
+use SL::Controller::CsvImport::Inventory;
 use SL::Controller::CsvImport::Shipto;
 use SL::Controller::CsvImport::Project;
 use SL::Controller::CsvImport::Order;
@@ -221,7 +222,7 @@ sub check_auth {
 sub check_type {
   my ($self) = @_;
 
-  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts customers_vendors addresses contacts projects orders);
+  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors addresses contacts projects orders);
   $self->type($::form->{profile}->{type});
 }
 
@@ -263,6 +264,7 @@ sub render_inputs {
             : $self->type eq 'addresses'         ? $::locale->text('CSV import: shipping addresses')
             : $self->type eq 'contacts'          ? $::locale->text('CSV import: contacts')
             : $self->type eq 'parts'             ? $::locale->text('CSV import: parts and services')
+            : $self->type eq 'inventories'       ? $::locale->text('CSV import: inventories')
             : $self->type eq 'projects'          ? $::locale->text('CSV import: projects')
             : $self->type eq 'orders'            ? $::locale->text('CSV import: orders')
             : die;
@@ -608,6 +610,7 @@ sub init_worker {
        : $self->{type} eq 'contacts'          ? SL::Controller::CsvImport::Contact->new(@args)
        : $self->{type} eq 'addresses'         ? SL::Controller::CsvImport::Shipto->new(@args)
        : $self->{type} eq 'parts'             ? SL::Controller::CsvImport::Part->new(@args)
+       : $self->{type} eq 'inventories'       ? SL::Controller::CsvImport::Inventory->new(@args)
        : $self->{type} eq 'projects'          ? SL::Controller::CsvImport::Project->new(@args)
        : $self->{type} eq 'orders'            ? SL::Controller::CsvImport::Order->new(@args)
        :                                        die "Program logic error";
diff --git a/SL/Controller/CsvImport/Inventory.pm b/SL/Controller/CsvImport/Inventory.pm
new file mode 100644 (file)
index 0000000..b4c7c40
--- /dev/null
@@ -0,0 +1,401 @@
+package SL::Controller::CsvImport::Inventory;
+
+
+use strict;
+
+use SL::Helper::Csv;
+use SL::Helper::DateTime;
+
+use SL::DBUtils;
+use SL::DB::Inventory;
+use SL::DB::Part;
+use SL::DB::Warehouse;
+use SL::DB::Bin;
+use SL::DB::TransferType;
+use SL::DB::Employee;
+
+use parent qw(SL::Controller::CsvImport::Base);
+
+
+use Rose::Object::MakeMethods::Generic
+(
+ 'scalar --get_set_init' => [ qw(settings parts_by warehouses_by bins_by) ],
+);
+
+
+sub init_class {
+  my ($self) = @_;
+  $self->class('SL::DB::Inventory');
+}
+
+sub init_profile {
+  my ($self) = @_;
+
+  my $profile = $self->SUPER::init_profile;
+  delete @{$profile}{qw(trans_id oe_id orderitems_id bestbefore trans_type_id project_id)};
+
+  return $profile;
+}
+
+sub init_settings {
+  my ($self) = @_;
+
+  return { map { ( $_ => $self->controller->profile->get($_) ) } qw(warehouse apply_warehouse
+                                                                    bin       apply_bin
+                                                                    comment   apply_comment) };
+}
+
+sub init_parts_by {
+  my ($self) = @_;
+
+  my $all_parts = SL::DB::Manager::Part->get_all;
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_parts } } ) } qw(id partnumber ean description) };
+}
+
+sub init_warehouses_by {
+  my ($self) = @_;
+
+  my $all_warehouses = SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]]);
+  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_warehouses } } ) } qw(id description) };
+}
+
+sub init_bins_by {
+  my ($self) = @_;
+
+  my $all_bins = SL::DB::Manager::Bin->get_all();
+  my $bins_by;
+  $bins_by->{'wh_id+id'}          = { map { ( $_->warehouse_id . '+' . $_->id          => $_ ) } @{ $all_bins } };
+  $bins_by->{'wh_id+description'} = { map { ( $_->warehouse_id . '+' . $_->description => $_ ) } @{ $all_bins } };
+
+  return $bins_by;
+}
+
+sub check_objects {
+  my ($self) = @_;
+
+  $self->controller->track_progress(phase => 'building data', progress => 0);
+
+  my $i;
+  my $num_data = scalar @{ $self->controller->data };
+  foreach my $entry (@{ $self->controller->data }) {
+    $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
+
+    $self->check_warehouse($entry);
+    $self->check_bin($entry);
+    $self->check_part($entry);
+    $self->check_qty($entry)            unless scalar @{ $entry->{errors} };
+    $self->handle_comment($entry);
+    $self->handle_employee($entry);
+    $self->handle_transfer_type($entry) unless scalar @{ $entry->{errors} };
+    $self->handle_shippingdate($entry);
+  } continue {
+    $i++;
+  }
+
+  $self->add_info_columns(qw(warehouse bin partnumber employee target_qty));
+}
+
+sub setup_displayable_columns {
+  my ($self) = @_;
+
+  $self->SUPER::setup_displayable_columns;
+
+  $self->add_displayable_columns({ name => 'bin',          description => $::locale->text('Bin')                     },
+                                 { name => 'bin_id',       description => $::locale->text('Bin (database ID)')       },
+                                 { name => 'chargenumber', description => $::locale->text('Charge number')           },
+                                 { name => 'comment',      description => $::locale->text('Comment')                 },
+                                 { name => 'employee_id',  description => $::locale->text('Employee (database ID)')  },
+                                 { name => 'partnumber',   description => $::locale->text('Part Number')             },
+                                 { name => 'parts_id',     description => $::locale->text('Part (database ID)')      },
+                                 { name => 'qty',          description => $::locale->text('qty (to transfer)')       },
+                                 { name => 'shippingdate', description => $::locale->text('Shipping date')           },
+                                 { name => 'target_qty',   description => $::locale->text('Target Qty')              },
+                                 { name => 'warehouse',    description => $::locale->text('Warehouse')               },
+                                 { name => 'warehouse_id', description => $::locale->text('Warehouse (database ID)') },
+                                );
+}
+
+sub check_warehouse {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # If warehouse from front-end is enforced for all transfers, use this, if valid.
+  if ($self->settings->{apply_warehouse} eq 'all') {
+    $object->warehouse_id(undef);
+    my $wh = $self->warehouses_by->{description}->{ $self->settings->{warehouse} };
+    if (!$wh) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
+      return 0;
+    }
+
+    $object->warehouse_id($wh->id);
+  }
+
+  # If warehouse from front-end is enforced for transfers with missing warehouse, use this, if valid.
+  if (    $self->settings->{apply_warehouse} eq 'missing'
+       && ! $object->warehouse_id
+       && ! $entry->{raw_data}->{warehouse} ) {
+    my $wh = $self->warehouses_by->{description}->{ $self->settings->{warehouse} };
+    if (!$wh) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
+      return 0;
+    }
+
+    $object->warehouse_id($wh->id);
+  }
+
+  # Check wether or not warehouse ID is valid.
+  if ($object->warehouse_id && !$self->warehouses_by->{id}->{ $object->warehouse_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
+    return 0;
+  }
+
+  # Map description to ID if given.
+  if (!$object->warehouse_id && $entry->{raw_data}->{warehouse}) {
+    my $wh = $self->warehouses_by->{description}->{ $entry->{raw_data}->{warehouse} };
+    if (!$wh) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
+      return 0;
+    }
+
+    $object->warehouse_id($wh->id);
+  }
+
+  if ($object->warehouse_id) {
+    $entry->{info_data}->{warehouse} = $self->warehouses_by->{id}->{ $object->warehouse_id }->description;
+  } else {
+    push @{ $entry->{errors} }, $::locale->text('Error: Warehouse not found');
+    return 0;
+  }
+
+  return 1;
+}
+
+# Check bin fior given warehouse, so check_warehouse must be called first.
+sub check_bin {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # If bin from front-end is enforced for all transfers, use this, if valid.
+  if ($self->settings->{apply_bin} eq 'all') {
+    $object->bin_id(undef);
+    my $bin = $self->bins_by->{'wh_id+description'}->{ $object->warehouse_id . '+' . $self->settings->{bin} };
+    if (!$bin) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
+      return 0;
+    }
+
+    $object->bin_id($bin->id);
+  }
+
+  # If bin from front-end is enforced for transfers with missing bin, use this, if valid.
+  if (    $self->settings->{apply_bin} eq 'missing'
+       && ! $object->bin_id
+       && ! $entry->{raw_data}->{bin} ) {
+    my $bin = $self->bins_by->{'wh_id+description'}->{ $object->warehouse_id . '+' . $self->settings->{bin} };
+    if (!$bin) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
+      return 0;
+    }
+
+    $object->bin_id($bin->id);
+  }
+
+  # Check wether or not bin ID is valid.
+  if ($object->bin_id && !$self->bins_by->{'wh_id+id'}->{ $object->warehouse_id . '+' . $object->bin_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
+    return 0;
+  }
+  
+  # Map description to ID if given.
+  if (!$object->bin_id && $entry->{raw_data}->{bin}) {
+    my $bin = $self->bins_by->{'wh_id+description'}->{ $object->warehouse_id . '+' . $entry->{raw_data}->{bin} };
+    if (!$bin) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
+      return 0;
+    }
+
+    $object->bin_id($bin->id);
+  }
+
+  if ($object->bin_id) {
+    $entry->{info_data}->{bin} = $self->bins_by->{'wh_id+id'}->{ $object->warehouse_id . '+' . $object->bin_id }->description;
+  } else {
+    push @{ $entry->{errors} }, $::locale->text('Error: Bin not found');
+    return 0;
+  }
+
+  return 1;
+}
+
+sub check_part {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # Check wether or non part ID is valid.
+  if ($object->parts_id && !$self->parts_by->{id}->{ $object->parts_id }) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
+    return 0;
+  }
+
+  # Map number to ID if given.
+  if (!$object->parts_id && $entry->{raw_data}->{partnumber}) {
+    my $part = $self->parts_by->{partnumber}->{ $entry->{raw_data}->{partnumber} };
+    if (!$part) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
+      return 0;
+    }
+
+    $object->parts_id($part->id);
+  }
+
+  if ($object->parts_id) {
+    $entry->{info_data}->{partnumber} = $self->parts_by->{id}->{ $object->parts_id }->partnumber;
+  } else {
+    push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
+    return 0;
+  }
+
+  return 1;
+}
+
+# This imports inventories when target_qty is given, transfers else.
+# So we get the actual qty in stock and transfer the difference in case of
+# a given target_qty
+sub check_qty{
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  if (! exists $entry->{raw_data}->{target_qty} && ! exists $entry->{raw_data}->{qty}) {
+    push @{ $entry->{errors} }, $::locale->text('Error: A quantity or a target quantity must be given.');
+    return 0;
+  }
+
+  if (exists $entry->{raw_data}->{target_qty} && exists $entry->{raw_data}->{qty}) {
+    push @{ $entry->{errors} }, $::locale->text('Error: a quantity and a target quantity could not be given both.');
+    return 0;
+  }
+
+  if (exists $entry->{raw_data}->{target_qty} && ($entry->{raw_data}->{target_qty} * 1) < 0) {
+    push @{ $entry->{errors} }, $::locale->text('Error: A negative target quantity is not allowed.');
+    return 0;
+  }
+
+  # Actual quantity is read from stock or is the result of transfers for the
+  # same part, warehouse and bin done before.
+  my $key = $object->parts_id . '+' . $object->warehouse_id . '+' . $object->bin_id;
+  if (!exists $self->{resulting_quantities}->{$key}) {
+    my $stock = $object->part->get_simple_stock;
+    my @stocked = grep { $_->{warehouse_id} == $object->warehouse_id && $_->{bin_id} == $object->bin_id } @$stock;
+    my $stocked_qty = 0;
+    foreach (@stocked) {
+      $stocked_qty += $stocked[0]->{sum} * 1;
+    }
+    $self->{resulting_quantities}->{$key} = $stocked_qty;
+  }
+  my $actual_qty = $self->{resulting_quantities}->{$key};
+
+  if (exists $entry->{raw_data}->{target_qty}) {
+    my $target_qty = $entry->{raw_data}->{target_qty} * 1;
+
+    $object->qty($target_qty - $actual_qty);
+    $self->add_columns(qw(qty));
+  }
+
+  if ($object->qty == 0) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Quantity to transfer is zero.');
+    return 0;
+  }
+
+  # Check if resulting quantity is below zero.
+  if ( ($actual_qty + $object->qty) < 0 ) {
+    push @{ $entry->{errors} }, $::locale->text('Error: Transfer would result in a negative target quantity.');
+    return 0;
+  }
+
+  $self->{resulting_quantities}->{$key} += $object->qty;
+  $entry->{info_data}->{target_qty} = $self->{resulting_quantities}->{$key};
+
+  return 1;
+}
+
+sub handle_comment {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # If comment from front-end is enforced for all transfers, use this, if valid.
+  if ($self->settings->{apply_comment} eq 'all') {
+    $object->comment($self->settings->{comment});
+  }
+
+  # If comment from front-end is enforced for transfers with missing comment, use this, if valid.
+  if ($self->settings->{apply_comment} eq 'missing' && ! $object->comment) {
+    $object->comment($self->settings->{comment});
+  }
+
+  return;
+}
+
+sub handle_transfer_type  {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  my $transfer_type = SL::DB::Manager::TransferType->find_by(description => 'correction',
+                                                             direction   => ($object->qty > 0)? 'in': 'out');
+  $object->trans_type($transfer_type);
+
+  return;
+}
+
+# ToDo: employee by name
+sub handle_employee {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # employee from front end if not given
+  if (!$object->employee_id) {
+    $object->employee_id($self->controller->{employee_id})
+  }
+
+  # employee from login if not given
+  if (!$object->employee_id) {
+    $object->employee_id(SL::DB::Manager::Employee->find_by(login => $::myconfig{login})->id);
+  }
+
+  if ($object->employee_id) {
+    $entry->{info_data}->{employee} = $object->employee->name;
+  }
+
+}
+
+sub handle_shippingdate {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  if (!$object->shippingdate) {
+    $object->shippingdate(DateTime->today_local);
+  }
+}
+
+sub save_objects {
+  my ($self, %params) = @_;
+
+  my $data = $params{data} || $self->controller->data;
+
+  foreach my $entry (@{ $data }) {
+    my ($trans_id) = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT nextval('id')|);
+    $entry->{object}->trans_id($trans_id);
+  }
+
+  $self->SUPER::save_objects(%params);
+}
+
+1;
index 7464dee..726bf14 100755 (executable)
@@ -238,7 +238,11 @@ $self->{texts} = {
   'Application Error. No Format given' => 'Fehler in der Anwendung. Das Ausgabeformat fehlt.',
   'Application Error. Wrong Format' => 'Fehler in der Anwendung. Falsches Format: ',
   'Apply to all parts'          => 'Bei allen Artikeln setzen',
+  'Apply to all transfers'      => 'Bei allen Lagerbewegungen setzen',
   'Apply to parts without buchungsgruppe' => 'Bei allen Artikeln ohne gültige Buchungsgruppe setzen',
+  'Apply to transfers without bin' => 'Bei allen Lagerbewegungen ohne Lagerplatz setzen',
+  'Apply to transfers without comment' => 'Bei allen Lagerbewegungen ohne Kommentar setzen',
+  'Apply to transfers without warehouse' => 'Bei allen Lagerbewegungen ohne Lager setzen',
   'Applying #1:'                => 'Führe #1 aus:',
   'Appointment Category'        => 'Termin Kategorie',
   'Appointments'                => 'Termine',
@@ -355,6 +359,7 @@ $self->{texts} = {
   'Billing/shipping address (street)' => 'Rechnungsadresse (Straße)',
   'Billing/shipping address (zipcode)' => 'Rechnungsadresse (PLZ)',
   'Bin'                         => 'Lagerplatz',
+  'Bin (database ID)'           => 'Lagerplatz (Datenbank-ID)',
   'Bin From'                    => 'Quelllagerplatz',
   'Bin List'                    => 'Lagerliste',
   'Bin To'                      => 'Ziellagerplatz',
@@ -416,6 +421,7 @@ $self->{texts} = {
   'CSV export -- options'       => 'CSV-Export -- Optionen',
   'CSV import: contacts'        => 'CSV-Import: Ansprechpersonen',
   'CSV import: customers and vendors' => 'CSV-Import: Kunden und Lieferanten',
+  'CSV import: inventories'     => 'CSV-Import: Lagerbewegungen/-bestände',
   'CSV import: orders'          => 'CSV-Import: Aufträge',
   'CSV import: parts and services' => 'CSV-Import: Waren und Dienstleistungen',
   'CSV import: projects'        => 'CSV-Import: Projekte',
@@ -859,6 +865,9 @@ $self->{texts} = {
   'Do not link to a project.'   => 'Nicht mit einem Projekt verknüpfen.',
   'Do not modify this position' => 'Diese Position nicht verändern',
   'Do not set default buchungsgruppe' => 'Nie Standardbuchungsgruppe setzen',
+  'Do not set this bin'         => 'Diesen Lagerplatz nicht setzen',
+  'Do not set this comment'     => 'Diesen Kommentar nicht setzen',
+  'Do not set this warehouse'   => 'Dieses Lager nicht setzen',
   'Do you really want do continue?' => 'Wollen Sie wirklich fortfahren?',
   'Do you really want to cancel?' => 'Wollen Sie wirklich abbrechen?',
   '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.',
@@ -1043,10 +1052,14 @@ $self->{texts} = {
   'Error when saving: #1'       => 'Fehler beim Speichern: #1',
   'Error with default taxzone'  => 'Ungültige Standardsteuerzone',
   'Error!'                      => 'Fehler!',
+  'Error: A negative target quantity is not allowed.' => 'Fehler: Eine negative Zielmenge ist nicht erlaubt.',
+  'Error: A quantity or a target quantity must be given.' => 'Fehler: Menge oder Zielmenge muss angegeben werden.',
+  'Error: Bin not found'        => 'Fehler: Lagerplatz nicht gefunden',
   'Error: Buchungsgruppe missing or invalid' => 'Fehler: Buchungsgruppe fehlt oder ungültig',
   'Error: Customer/vendor missing' => 'Fehler: Kunde/Lieferant fehlt',
   'Error: Customer/vendor not found' => 'Fehler: Kunde/Lieferant nicht gefunden',
   'Error: Gender (cp_gender) missing or invalid' => 'Fehler: Geschlecht (cp_gender) fehlt oder ungültig',
+  'Error: Invalid bin'          => 'Fehler: Ungültiger Lagerplatz',
   'Error: Invalid business'     => 'Fehler: Kunden-/Lieferantentyp ungültig',
   'Error: Invalid contact'      => 'Fehler: Ansprechperson ungültig',
   'Error: Invalid currency'     => 'Fehler: ungültige Währung',
@@ -1064,9 +1077,14 @@ $self->{texts} = {
   'Error: Invalid shipto'       => 'Fehler: Lieferadresse ungültig',
   'Error: Invalid tax zone'     => 'Fehler: Steuerzone ungültig',
   'Error: Invalid vendor in column make_#1' => 'Fehler: Lieferant ungültig in Spalte make_#1',
+  'Error: Invalid warehouse'    => 'Fehler: Ungültiges Lager',
   'Error: Name missing'         => 'Fehler: Name fehlt',
   'Error: Part not found'       => 'Fehler: Artikel nicht gefunden',
+  'Error: Quantity to transfer is zero.' => 'Fehler: Zu bewegende Menge ist Null.',
+  'Error: Transfer would result in a negative target quantity.' => 'Fehler: Lagerbewegung würde zu einer negativen Zielmenge führen.',
   'Error: Unit missing or invalid' => 'Fehler: Einheit fehlt oder ungültig',
+  'Error: Warehouse not found'  => 'Fehler: Lager nicht gefunden',
+  'Error: a quantity and a target quantity could not be given both.' => 'Fehler: Menge und Zielmenge können nicht beide angegeben werden.',
   'Error: this feature requires that articles with a time-based unit (e.g. \'h\' or \'min\') exist.' => 'Fehler: dieses Feature setzt voraus, dass Artikel mit einer Zeit-basierenden Einheit (z.B. "Std") existieren.',
   'Errors'                      => 'Fehler',
   'Ertrag'                      => 'Ertrag',
@@ -1327,6 +1345,7 @@ $self->{texts} = {
   'Invalid variable #1'         => 'Ungültige Variable #1',
   'Invdate'                     => 'Rechnungsdatum',
   'Invdate from'                => 'Rechnungen von',
+  'Inventories'                 => 'Lagerbewegungen/-bestände',
   'Inventory'                   => 'Inventar',
   'Inventory Account'           => 'Warenbestand',
   'Inventory quantity must be zero before you can set this assembly obsolete!' => 'Bevor dieses Erzeugnis als ungültig markiert werden kann, muß das Inventar auf Null sein!',
@@ -1678,6 +1697,7 @@ $self->{texts} = {
   'On'                          => 'An',
   'On Hand'                     => 'Auf Lager',
   'On Order'                    => 'Ist bestellt',
+  'One of the columns "qty" or "target_qty" must be given. If "target_qty" is given, the quantity to transfer for each transfer will be calculate, so that the quantity for this part, warehouse and bin will result in the given "target_qty" after each transfer.' => 'Eine der Spalten "qty" oder "target_qty" muss angegeben werden. Wird "target_qty" angegeben, so wird die zu bewegende Menge für jede Lagerbewegung so berechnet, dass die Lagermenge für diesen Artikel, Lager und Lagerplatz nach jeder Lagerbewegung der angebenen Zielmenge entspricht.',
   'One or more Perl modules missing' => 'Ein oder mehr Perl-Module fehlen',
   'Onhand only sets the quantity in master data, not in inventory. This is only a legacy info field and will be overwritten as soon as a inventory transfer happens.' => 'Das Import-Feld Auf Lager setzt nur die Menge in den Stammdaten, nicht im Lagerbereich. Dies ist historisch gewachsen nur ein Informationsfeld was mit dem tatsächlichen Wert überschrieben wird, sobald eine wirkliche Lagerbewegung stattfindet (DB-Trigger).',
   'Only Warnings and Errors'    => 'Nur Warnungen und Fehler',
@@ -2222,6 +2242,7 @@ $self->{texts} = {
   'Ship via'                    => 'Transportmittel',
   'Shipping Address'            => 'Lieferadresse',
   'Shipping Point'              => 'Versandort',
+  'Shipping date'               => 'Lieferdatum',
   'Shipto'                      => 'Lieferanschriften',
   'Shipto deleted.'             => 'Lieferadresse gelöscht',
   'Shipto is in use and was flagged invalid.' => 'Lieferadresse ist noch in Verwendung, und wurde als ungültig markiert.',
@@ -2356,6 +2377,7 @@ $self->{texts} = {
   'TOP100'                      => 'Top 100',
   'TOTAL'                       => 'TOTAL',
   'Tab'                         => 'Tabulator',
+  'Target Qty'                  => 'Zielmenge',
   'Target bank account'         => 'Zielkonto',
   'Target table'                => 'Zieltabelle',
   'Task Server is not running, starting it now. If this does not change, please check your task server config' => 'Der Taskserver läuft nicht, starte ihn jetzt. Das kann ein paar Sekunden dauern. Sollte das nicht funktionieren, prüfen Sie bitte die Taskserver-Konfiguration.',
@@ -2944,6 +2966,7 @@ $self->{texts} = {
   'WARN: Tax included value!'   => 'ACHTUNG: Steuer im Preis inbegriffen!',
   'WHJournal'                   => 'Lagerbuchungen',
   'Warehouse'                   => 'Lager',
+  'Warehouse (database ID)'     => 'Lager (Datenbank-ID)',
   'Warehouse From'              => 'Quelllager',
   'Warehouse Migration'         => 'Lagermigration',
   'Warehouse To'                => 'Ziellager',
@@ -3194,6 +3217,7 @@ $self->{texts} = {
   'purchase_delivery_order_list' => 'lieferscheinliste_einkauf',
   'purchase_order'              => 'Auftrag',
   'purchase_order_list'         => 'lieferantenauftragsliste',
+  'qty (to transfer)'           => 'zu bewegende Menge',
   'quarter'                     => 'Vierteljährliche (quartalsweise) Abgabe',
   'quotation_list'              => 'angebotsliste',
   'release_material'            => 'Materialausgabebe',
index a6ccd05..fca8ec0 100644 (file)
@@ -211,7 +211,11 @@ $self->{texts} = {
   'Application Error. No Format given' => '',
   'Application Error. Wrong Format' => '',
   'Apply to all parts'          => '',
+  'Apply to all transfers'      => '',
   'Apply to parts without buchungsgruppe' => '',
+  'Apply to transfers without bin' => '',
+  'Apply to transfers without comment' => '',
+  'Apply to transfers without warehouse' => '',
   'Applying #1:'                => '',
   'Appointment Category'        => '',
   'Appointments'                => '',
@@ -307,6 +311,7 @@ $self->{texts} = {
   'Billing/shipping address (street)' => '',
   'Billing/shipping address (zipcode)' => '',
   'Bin'                         => '',
+  'Bin (database ID)'           => '',
   'Bin From'                    => '',
   'Bin List'                    => '',
   'Bin To'                      => '',
@@ -365,6 +370,7 @@ $self->{texts} = {
   'CSV export -- options'       => '',
   'CSV import: contacts'        => '',
   'CSV import: customers and vendors' => '',
+  'CSV import: inventories'     => '',
   'CSV import: orders'          => '',
   'CSV import: parts and services' => '',
   'CSV import: projects'        => '',
@@ -724,6 +730,9 @@ $self->{texts} = {
   'Do not change the tax rate of taxkey 0.' => '',
   'Do not check for duplicates' => '',
   'Do not set default buchungsgruppe' => '',
+  'Do not set this bin'         => '',
+  'Do not set this comment'     => '',
+  'Do not set this warehouse'   => '',
   '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.' => '',
   '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.' => '',
   'Do you really want to delete AP transaction #1?' => '',
@@ -881,10 +890,14 @@ $self->{texts} = {
   'Error message from the database: #1' => '',
   'Error when saving: #1'       => '',
   'Error!'                      => '',
+  'Error: A negative target quantity is not allowed.' => '',
+  'Error: A quantity or a target quantity must be given.' => '',
+  'Error: Bin not found'        => '',
   'Error: Buchungsgruppe missing or invalid' => '',
   'Error: Customer/vendor missing' => '',
   'Error: Customer/vendor not found' => '',
   'Error: Gender (cp_gender) missing or invalid' => '',
+  'Error: Invalid bin'          => '',
   'Error: Invalid business'     => '',
   'Error: Invalid contact'      => '',
   'Error: Invalid currency'     => '',
@@ -902,9 +915,14 @@ $self->{texts} = {
   'Error: Invalid shipto'       => '',
   'Error: Invalid tax zone'     => '',
   'Error: Invalid vendor in column make_#1' => '',
+  'Error: Invalid warehouse'    => '',
   'Error: Name missing'         => '',
   'Error: Part not found'       => '',
+  'Error: Quantity to transfer is zero.' => '',
+  'Error: Transfer would result in a negative target quantity.' => '',
   'Error: Unit missing or invalid' => '',
+  'Error: Warehouse not found'  => '',
+  'Error: a quantity and a target quantity could not be given both.' => '',
   'Errors'                      => '',
   'Ertrag'                      => '',
   'Ertrag prozentual'           => '',
@@ -1135,6 +1153,7 @@ $self->{texts} = {
   'Invalid transactions'        => '',
   'Invdate'                     => '',
   'Invdate from'                => '',
+  'Inventories'                 => '',
   'Inventory'                   => '',
   'Inventory Account'           => '',
   'Inventory quantity must be zero before you can set this assembly obsolete!' => '',
@@ -1435,6 +1454,7 @@ $self->{texts} = {
   'On'                          => '',
   'On Hand'                     => '',
   'On Order'                    => '',
+  'One of the columns "qty" or "target_qty" must be given. If "target_qty" is given, the quantity to transfer for each transfer will be calculate, so that the quantity for this part, warehouse and bin will result in the given "target_qty" after each transfer.' => '',
   'One or more Perl modules missing' => '',
   'Only Warnings and Errors'    => '',
   'Only due follow-ups'         => '',
@@ -1869,6 +1889,7 @@ $self->{texts} = {
   'Ship via'                    => '',
   'Shipping Address'            => '',
   'Shipping Point'              => '',
+  'Shipping date'               => '',
   'Shipto'                      => '',
   'Shipto deleted.'             => '',
   'Shipto is in use and was flagged invalid.' => '',
@@ -1989,6 +2010,7 @@ $self->{texts} = {
   'TOP100'                      => '',
   'TOTAL'                       => '',
   'Tab'                         => '',
+  'Target Qty'                  => '',
   'Target bank account'         => '',
   'Target table'                => '',
   'Task Server is not running, starting it now. If this does not change, please check your task server config' => '',
@@ -2452,6 +2474,7 @@ $self->{texts} = {
   'Von Konto: '                 => '',
   'WHJournal'                   => '',
   'Warehouse'                   => '',
+  'Warehouse (database ID)'     => '',
   'Warehouse From'              => '',
   'Warehouse Migration'         => '',
   'Warehouse To'                => '',
@@ -2689,6 +2712,7 @@ $self->{texts} = {
   'purchase_delivery_order_list' => '',
   'purchase_order'              => '',
   'purchase_order_list'         => '',
+  'qty (to transfer)'           => '',
   'quarter'                     => '',
   'quotation_list'              => '',
   'release_material'            => '',
index bce61f9..c9397c3 100644 (file)
@@ -745,6 +745,11 @@ module=controller.pl
 action=CsvImport/new
 profile.type=parts
 
+[System--Import CSV--Inventories]
+module=controller.pl
+action=CsvImport/new
+profile.type=inventories
+
 [System--Import CSV--Projects]
 module=controller.pl
 action=CsvImport/new
diff --git a/templates/webpages/csv_import/_form_inventories.html b/templates/webpages/csv_import/_form_inventories.html
new file mode 100644 (file)
index 0000000..cd171a0
--- /dev/null
@@ -0,0 +1,30 @@
+[% USE LxERP %]
+[% USE L %]
+<tr>
+ <th align="right" valign="top">[%- LxERP.t8('Warehouse') %]/[%- LxERP.t8('Bin') %]:</th>
+ <td colspan="2" valign="top">
+  [% L.input_tag('settings.warehouse', SELF.profile.get('warehouse'), style = 'width: 300px') %]
+  <br>
+  [% opts = [ [ 'never', LxERP.t8('Do not set this warehouse') ],[ 'missing', LxERP.t8('Apply to transfers without warehouse') ], [ 'all', LxERP.t8('Apply to all transfers') ] ] %]
+  [% L.select_tag('settings.apply_warehouse', opts, default = SELF.profile.get('apply_warehouse'), style = 'width: 300px') %]
+ </td>
+ <td colspan="8" valign="top">
+  [% L.input_tag('settings.bin', SELF.profile.get('bin'), style = 'width: 300px') %]
+  <br>
+  [% opts = [ [ 'never', LxERP.t8('Do not set this bin') ], [ 'missing', LxERP.t8('Apply to transfers without bin') ], [ 'all', LxERP.t8('Apply to all transfers') ] ] %]
+  [% L.select_tag('settings.apply_bin', opts, default = SELF.profile.get('apply_bin'), style = 'width: 300px') %]
+ </td>
+</tr>
+
+<tr>
+</tr>
+
+<tr>
+ <th align="right" valign="top">[%- LxERP.t8('Comment') %]:</th>
+ <td colspan="10" valign="top">
+  [% L.input_tag('settings.comment', SELF.profile.get('comment'), style = 'width: 300px') %]
+  <br>
+  [% opts = [ [ 'never', LxERP.t8('Do not set this comment') ], [ 'missing', LxERP.t8('Apply to transfers without comment') ], [ 'all', LxERP.t8('Apply to all transfers') ] ] %]
+  [% L.select_tag('settings.apply_comment', opts, default = SELF.profile.get('apply_comment'), style = 'width: 300px') %]
+ </td>
+</tr>
index 8702de9..8535f1a 100644 (file)
     [% LxERP.t8("Assemblies can not be imported (yet). But the type column is used for sanity checks on price updates in order to prevent that articles with the wrong type will be updated.") %]
    </p>
 
+[%- ELSIF SELF.type == 'inventories' %]
+   <p>
+    [%- LxERP.t8('One of the columns "qty" or "target_qty" must be given. If "target_qty" is given, the quantity to transfer for each transfer will be calculate, so that the quantity for this part, warehouse and bin will result in the given "target_qty" after each transfer.') %]
+   </p>
+
 [%- ELSIF SELF.type == 'orders' %]
    <p>
     [1]:
  [%- INCLUDE 'csv_import/_form_customers_vendors.html' %]
 [%- ELSIF SELF.type == 'contacts' %]
  [%- INCLUDE 'csv_import/_form_contacts.html' %]
+[%- ELSIF SELF.type == 'inventories' %]
+ [%- INCLUDE 'csv_import/_form_inventories.html' %]
 [%- ELSIF SELF.type == 'orders' %]
  [%- INCLUDE 'csv_import/_form_orders.html' %]
 [%- END %]