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);
__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('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 {
$::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 };
$::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',
$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 {
qty => $qty,
unit => $self->unit,
transfer_type => 'stock',
+ transfer_type_id => $::form->{transfer_type_id},
chargenumber => $::form->{chargenumber},
bestbefore => $::form->{bestbefore},
- ean => $::form->{ean},
comment => $::form->{comment},
});
1;
- } or do { $transfer_error = $EVAL_ERROR->getMessage; }
+ } or do { $transfer_error = $EVAL_ERROR->error; }
});
if (!$transfer_error) {
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 '';
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()
$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 {
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;
}
}
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 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} = $_;
}
- # 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;
}
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,
$bar->add(
action => [
t8('Save'),
+ checks => [ 'kivi.Inventory.check_stocktaking_qty_threshold' ],
call => [ 'kivi.Inventory.save_stocktaking' ],
accesskey => 'enter',
],
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.
+This actions should contain "stocktaking" in their name.
=back