Volltext-Suche: Rose-Beziehung von File zu full_text
[kivitendo-erp.git] / SL / Controller / Inventory.pm
index 792aa2c..a4c11f6 100644 (file)
@@ -20,6 +20,7 @@ use SL::DBUtils;
 use SL::Helper::Flash;
 use SL::Controller::Helper::ReportGenerator;
 use SL::Controller::Helper::GetModels;
 use SL::Helper::Flash;
 use SL::Controller::Helper::ReportGenerator;
 use SL::Controller::Helper::GetModels;
+use List::MoreUtils qw(uniq);
 
 use English qw(-no_match_vars);
 
 
 use English qw(-no_match_vars);
 
@@ -30,13 +31,13 @@ use Rose::Object::MakeMethods::Generic (
 
 __PACKAGE__->run_before('_check_auth');
 __PACKAGE__->run_before('_check_warehouses');
 
 __PACKAGE__->run_before('_check_auth');
 __PACKAGE__->run_before('_check_warehouses');
-__PACKAGE__->run_before('load_part_from_form',   only => [ qw(stock_in part_changed mini_stock stock stocktaking_part_changed save_stocktaking) ]);
-__PACKAGE__->run_before('load_unit_from_form',   only => [ qw(stock_in part_changed mini_stock stock stocktaking_part_changed save_stocktaking) ]);
-__PACKAGE__->run_before('load_wh_from_form',     only => [ qw(stock_in warehouse_changed stock stocktaking save_stocktaking) ]);
-__PACKAGE__->run_before('load_bin_from_form',    only => [ qw(stock_in stock stocktaking save_stocktaking) ]);
+__PACKAGE__->run_before('load_part_from_form',   only => [ qw(stock_in part_changed mini_stock stock stocktaking_part_changed stocktaking_get_warn_qty_threshold save_stocktaking) ]);
+__PACKAGE__->run_before('load_unit_from_form',   only => [ qw(stock_in part_changed mini_stock stock stocktaking_part_changed stocktaking_get_warn_qty_threshold save_stocktaking) ]);
+__PACKAGE__->run_before('load_wh_from_form',     only => [ qw(stock_in warehouse_changed stock stocktaking stocktaking_get_warn_qty_threshold save_stocktaking) ]);
+__PACKAGE__->run_before('load_bin_from_form',    only => [ qw(stock_in stock stocktaking stocktaking_get_warn_qty_threshold save_stocktaking) ]);
 __PACKAGE__->run_before('set_target_from_part',  only => [ qw(part_changed) ]);
 __PACKAGE__->run_before('mini_stock',            only => [ qw(stock_in mini_stock) ]);
 __PACKAGE__->run_before('set_target_from_part',  only => [ qw(part_changed) ]);
 __PACKAGE__->run_before('mini_stock',            only => [ qw(stock_in mini_stock) ]);
-__PACKAGE__->run_before('sanitize_target',       only => [ qw(stock_usage stock_in warehouse_changed part_changed stocktaking stocktaking_part_changed save_stocktaking) ]);
+__PACKAGE__->run_before('sanitize_target',       only => [ qw(stock_usage stock_in warehouse_changed part_changed stocktaking stocktaking_part_changed stocktaking_get_warn_qty_threshold save_stocktaking) ]);
 __PACKAGE__->run_before('set_layout');
 
 sub action_stock_in {
 __PACKAGE__->run_before('set_layout');
 
 sub action_stock_in {
@@ -44,6 +45,12 @@ sub action_stock_in {
 
   $::form->{title}   = t8('Stock');
 
 
   $::form->{title}   = t8('Stock');
 
+  # Sometimes we want to open stock_in with a part already selected, but only
+  # the parts_id is passed in the url (and not also warehouse, bin and unit).
+  # Setting select_default_bin in the form will make sure the default warehouse
+  # and bin of that part will already be preselected, as normally
+  # set_target_from_part is only called when a part is changed.
+  $self->set_target_from_part if $::form->{select_default_bin};
   $::request->layout->focus('#part_id_name');
   my $transfer_types = WH->retrieve_transfer_types('in');
   map { $_->{description} = $main::locale->text($_->{description}) } @{ $transfer_types };
   $::request->layout->focus('#part_id_name');
   my $transfer_types = WH->retrieve_transfer_types('in');
   map { $_->{description} = $main::locale->text($_->{description}) } @{ $transfer_types };
@@ -58,7 +65,6 @@ sub action_stock_usage {
 
   $::form->get_lists('warehouses' => { 'key'    => 'WAREHOUSES',
                                        'bins'   => 'BINS', });
 
   $::form->get_lists('warehouses' => { 'key'    => 'WAREHOUSES',
                                        'bins'   => 'BINS', });
-  $::request->layout->use_javascript("${_}.js") for qw(kivi.PartsWarehouse);
 
   $self->setup_stock_usage_action_bar;
   $self->render('inventory/warehouse_usage',
 
   $self->setup_stock_usage_action_bar;
   $self->render('inventory/warehouse_usage',
@@ -388,7 +394,7 @@ sub make_row_result {
        $row->{outcorrection}->{data} - $row->{incorrection}->{data};
   $row->{averconsumed}->{data} = $row->{consumed}->{data}*30/$days ;
   map { $row->{$_}->{data} = $form->format_amount($myconfig,$row->{$_}->{data},2); } $self->getnumcolumns();
        $row->{outcorrection}->{data} - $row->{incorrection}->{data};
   $row->{averconsumed}->{data} = $row->{consumed}->{data}*30/$days ;
   map { $row->{$_}->{data} = $form->format_amount($myconfig,$row->{$_}->{data},2); } $self->getnumcolumns();
-  $row->{partnumber}->{link} = 'controller.pl?action=Part/edit&part.id' . $partid;
+  $row->{partnumber}->{link} = 'controller.pl?action=Part/edit&part.id=' . $partid;
 }
 
 sub action_stock {
 }
 
 sub action_stock {
@@ -411,13 +417,13 @@ sub action_stock {
           qty           => $qty,
           unit          => $self->unit,
           transfer_type => 'stock',
           qty           => $qty,
           unit          => $self->unit,
           transfer_type => 'stock',
+          transfer_type_id => $::form->{transfer_type_id},
           chargenumber  => $::form->{chargenumber},
           bestbefore    => $::form->{bestbefore},
           chargenumber  => $::form->{chargenumber},
           bestbefore    => $::form->{bestbefore},
-          ean           => $::form->{ean},
           comment       => $::form->{comment},
         });
         1;
           comment       => $::form->{comment},
         });
         1;
-      } or do { $transfer_error = $EVAL_ERROR->getMessage; }
+      } or do { $transfer_error = $EVAL_ERROR->error; }
     });
 
     if (!$transfer_error) {
     });
 
     if (!$transfer_error) {
@@ -498,6 +504,9 @@ sub action_stocktaking {
 sub action_save_stocktaking {
   my ($self) = @_;
 
 sub action_save_stocktaking {
   my ($self) = @_;
 
+  return $self->js->flash('error', t8('Please choose a part.'))->render()
+    if !$::form->{part_id};
+
   return $self->js->flash('error', t8('A target quantitiy has to be given'))->render()
     if $::form->{target_qty} eq '';
 
   return $self->js->flash('error', t8('A target quantitiy has to be given'))->render()
     if $::form->{target_qty} eq '';
 
@@ -565,7 +574,7 @@ sub action_save_stocktaking {
         stocktaking_cutoff_date => $::form->{cutoff_date_as_date},
       });
       1;
         stocktaking_cutoff_date => $::form->{cutoff_date_as_date},
       });
       1;
-    } or do { $transfer_error = $EVAL_ERROR->getMessage; }
+    } or do { $transfer_error = $EVAL_ERROR->error; }
   });
 
   return $self->js->flash('error', $transfer_error)->render()
   });
 
   return $self->js->flash('error', $transfer_error)->render()
@@ -603,6 +612,35 @@ sub action_stocktaking_journal {
   $self->prepare_stocktaking_report(full => 1);
   $self->report_generator_list_objects(report => $self->{report}, objects => $self->stocktaking_models->get);
 }
   $self->prepare_stocktaking_report(full => 1);
   $self->report_generator_list_objects(report => $self->{report}, objects => $self->stocktaking_models->get);
 }
+
+sub action_stocktaking_get_warn_qty_threshold {
+  my ($self) = @_;
+
+  return $_[0]->render(\ !!0, { type => 'text' }) if !$::form->{part_id};
+  return $_[0]->render(\ !!0, { type => 'text' }) if $::form->{target_qty} eq '';
+  return $_[0]->render(\ !!0, { type => 'text' }) if 0 == $::instance_conf->get_stocktaking_qty_threshold;
+
+  my $target_qty  = $::form->parse_amount(\%::myconfig, $::form->{target_qty});
+  my $stocked_qty = _get_stocked_qty($self->part,
+                                     warehouse_id => $self->warehouse->id,
+                                     bin_id       => $self->bin->id,
+                                     chargenumber => $::form->{chargenumber},
+                                     bestbefore   => $::form->{bestbefore},);
+  my $stocked_qty_in_form_units = $self->part->unit_obj->convert_to($stocked_qty, $self->unit);
+  my $qty        = $target_qty - $stocked_qty_in_form_units;
+  $qty           = abs($qty);
+
+  my $warn;
+  if ($qty > $::instance_conf->get_stocktaking_qty_threshold) {
+    $warn  = t8('The target quantity of #1 differs more than the threshold quantity of #2.',
+                $::form->{target_qty} . " " . $self->unit->name,
+                $::form->format_amount(\%::myconfig, $::instance_conf->get_stocktaking_qty_threshold, 2));
+    $warn .= "\n";
+    $warn .= t8('Choose "continue" if you want to use this value. Choose "cancel" otherwise.');
+  }
+  return $_[0]->render(\ $warn, { type => 'text' });
+}
+
 #================================================================
 
 sub _check_auth {
 #================================================================
 
 sub _check_auth {
@@ -666,7 +704,7 @@ sub init_stocktaking_cutoff_date {
   my $now    = DateTime->now_local;
   my $cutoff = DateTime->new(year => $now->year, month => 12, day => 31);
   if ($now->month < 1) {
   my $now    = DateTime->now_local;
   my $cutoff = DateTime->new(year => $now->year, month => 12, day => 31);
   if ($now->month < 1) {
-    $cutoff->substract(years => 1);
+    $cutoff->subtract(years => 1);
   }
   return $cutoff;
 }
   }
   return $cutoff;
 }
@@ -696,7 +734,7 @@ sub sanitize_target {
 }
 
 sub load_part_from_form {
 }
 
 sub load_part_from_form {
-  $_[0]->part(SL::DB::Manager::Part->find_by_or_create(id => $::form->{part_id}));
+  $_[0]->part(SL::DB::Manager::Part->find_by_or_create(id => $::form->{part_id}||undef));
 }
 
 sub load_unit_from_form {
 }
 
 sub load_unit_from_form {
@@ -750,22 +788,55 @@ sub build_unit_select {
 sub mini_journal {
   my ($self) = @_;
 
 sub mini_journal {
   my ($self) = @_;
 
-  # get last 10 transaction ids
-  my $query = 'SELECT trans_id, max(itime) FROM inventory GROUP BY trans_id ORDER BY max(itime) DESC LIMIT 10';
-  my @ids = selectall_array_query($::form, $::form->get_standard_dbh, $query);
+  # We want to fetch the last 10 inventory events (inventory rows with the same trans_id)
+  # To prevent a Seq Scan on inventory set an index on inventory.itime
+  # Each event may have one (transfer_in/out) or two (transfer) inventory rows
+  # So fetch the last 20, group by trans_id, limit to the last 10 trans_ids,
+  # and then extract the inventory ids from those 10 trans_ids
+  # By querying Inventory->get_all via the id instead of trans_id we can make
+  # use of the existing index on id
 
 
-  my $objs;
-  $objs = SL::DB::Manager::Inventory->get_all(query => [ trans_id => \@ids ]) if @ids;
+  # inventory ids of the most recent 10 inventory trans_ids
+  my $query = <<SQL;
+with last_inventories as (
+   select id,
+          trans_id,
+          itime
+     from inventory
+ order by itime desc
+    limit 20
+),
+grouped_ids as (
+   select trans_id,
+          array_agg(id) as ids
+     from last_inventories
+ group by trans_id
+ order by max(itime)
+     desc limit 10
+)
+select unnest(ids)
+  from grouped_ids
+ limit 20  -- so the planner knows how many ids to expect, the cte is an optimisation fence
+SQL
 
 
-  # at most 2 of them belong to a transaction and the qty determins in or out.
-  # sort them for display
+  my $objs  = SL::DB::Manager::Inventory->get_all(
+    query        => [ id => [ \"$query" ] ],                           # " make emacs happy
+    with_objects => [ 'parts', 'trans_type', 'bin', 'bin.warehouse' ], # prevent lazy loading in template
+    sort_by      => 'itime DESC',
+  );
+  # remember order of trans_ids from query, for ordering hash later
+  my @sorted_trans_ids = uniq map { $_->trans_id } @$objs;
+
+  # at most 2 of them belong to a transaction and the qty determines in or out.
   my %transactions;
   for (@$objs) {
     $transactions{ $_->trans_id }{ $_->qty > 0 ? 'in' : 'out' } = $_;
     $transactions{ $_->trans_id }{base} = $_;
   }
   my %transactions;
   for (@$objs) {
     $transactions{ $_->trans_id }{ $_->qty > 0 ? 'in' : 'out' } = $_;
     $transactions{ $_->trans_id }{base} = $_;
   }
-  # and get them into order again
-  my @sorted = map { $transactions{$_} } @ids;
+
+  # because the inventory transactions were built in a hash, we need to sort the
+  # hash by using the original sort order of the trans_ids
+  my @sorted = map { $transactions{$_} } @sorted_trans_ids;
 
   return \@sorted;
 }
 
   return \@sorted;
 }
@@ -882,7 +953,7 @@ sub _already_counted {
 
   my %bestbefore_filter;
   if ($::instance_conf->get_show_bestbefore) {
 
   my %bestbefore_filter;
   if ($::instance_conf->get_show_bestbefore) {
-    %bestbefore_filter = (bestbefore => $params{bestbefore});
+    %bestbefore_filter = (bestbefore => ($params{bestbefore} || undef));
   }
 
   SL::DB::Manager::Stocktaking->get_all(query => [and => [parts_id     => $part->id,
   }
 
   SL::DB::Manager::Stocktaking->get_all(query => [and => [parts_id     => $part->id,
@@ -930,6 +1001,7 @@ sub setup_stock_stocktaking_action_bar {
     $bar->add(
       action => [
         t8('Save'),
     $bar->add(
       action => [
         t8('Save'),
+        checks    => [ 'kivi.Inventory.check_stocktaking_qty_threshold' ],
         call      => [ 'kivi.Inventory.save_stocktaking' ],
         accesskey => 'enter',
       ],
         call      => [ 'kivi.Inventory.save_stocktaking' ],
         accesskey => 'enter',
       ],
@@ -1016,9 +1088,16 @@ and the current employee. The history is displayed via javascript.
 
 This action is called after the user selected or changed the part.
 
 
 This action is called after the user selected or changed the part.
 
+=item C<action_stocktaking_get_warn_qty_threshold>
+
+This action checks if a warning should be shown and returns the warning text via
+ajax. The warning will be shown if the given target value is greater than the
+threshold given in the client configuration.
+
 =item C<is_stocktaking>
 
 This is a method to check if actions are called from stocktaking form.
 =item C<is_stocktaking>
 
 This is a method to check if actions are called from stocktaking form.
+This actions should contain "stocktaking" in their name.
 
 =back
 
 
 =back