Projekt

Allgemein

Profil

Herunterladen (35,6 KB) Statistiken
| Zweig: | Markierung: | Revision:
666d4cad Sven Schöling
package SL::Controller::Inventory;

use strict;
use warnings;
34035b33 Martin Helmling
use POSIX qw(strftime);
666d4cad Sven Schöling
use parent qw(SL::Controller::Base);

use SL::DB::Inventory;
51072516 Bernd Bleßmann
use SL::DB::Stocktaking;
666d4cad Sven Schöling
use SL::DB::Part;
use SL::DB::Warehouse;
use SL::DB::Unit;
51072516 Bernd Bleßmann
use SL::DB::Default;
666d4cad Sven Schöling
use SL::WH;
34035b33 Martin Helmling
use SL::ReportGenerator;
666d4cad Sven Schöling
use SL::Locale::String qw(t8);
a97574b8 Sven Schöling
use SL::Presenter::Tag qw(select_tag);
666d4cad Sven Schöling
use SL::DBUtils;
use SL::Helper::Flash;
34035b33 Martin Helmling
use SL::Controller::Helper::ReportGenerator;
51072516 Bernd Bleßmann
use SL::Controller::Helper::GetModels;
666d4cad Sven Schöling
81b7704d Bernd Bleßmann
use English qw(-no_match_vars);

666d4cad Sven Schöling
use Rose::Object::MakeMethods::Generic (
51072516 Bernd Bleßmann
'scalar --get_set_init' => [ qw(warehouses units is_stocktaking stocktaking_models stocktaking_cutoff_date) ],
666d4cad Sven Schöling
'scalar' => [ qw(warehouse bin unit part) ],
);

__PACKAGE__->run_before('_check_auth');
__PACKAGE__->run_before('_check_warehouses');
51072516 Bernd Bleßmann
__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) ]);
666d4cad Sven Schöling
__PACKAGE__->run_before('set_target_from_part', only => [ qw(part_changed) ]);
5f543a5e Sven Schöling
__PACKAGE__->run_before('mini_stock', only => [ qw(stock_in mini_stock) ]);
51072516 Bernd Bleßmann
__PACKAGE__->run_before('sanitize_target', only => [ qw(stock_usage stock_in warehouse_changed part_changed stocktaking stocktaking_part_changed save_stocktaking) ]);
666d4cad Sven Schöling
__PACKAGE__->run_before('set_layout');

sub action_stock_in {
my ($self) = @_;

$::form->{title} = t8('Stock');

$::request->layout->focus('#part_id_name');
5e34b4af Martin Helmling
my $transfer_types = WH->retrieve_transfer_types('in');
map { $_->{description} = $main::locale->text($_->{description}) } @{ $transfer_types };
fedfc383 Moritz Bunkus
$self->setup_stock_in_action_bar;
5e34b4af Martin Helmling
$self->render('inventory/warehouse_selection_stock', title => $::form->{title}, TRANSFER_TYPES => $transfer_types );
666d4cad Sven Schöling
}

34035b33 Martin Helmling
sub action_stock_usage {
my ($self) = @_;

$::form->{title} = t8('UsageE');

$::form->get_lists('warehouses' => { 'key' => 'WAREHOUSES',
'bins' => 'BINS', });
$::request->layout->use_javascript("${_}.js") for qw(kivi.PartsWarehouse);

fedfc383 Moritz Bunkus
$self->setup_stock_usage_action_bar;
34035b33 Martin Helmling
$self->render('inventory/warehouse_usage',
title => $::form->{title},
year => DateTime->today->year,
WAREHOUSES => $::form->{WAREHOUSES},
WAREHOUSE_FILTER => 1,
warehouse_id => 0,
bin_id => 0
);

}

sub getnumcolumns {
my ($self) = @_;
70edaa5f Geoffrey Richardson
return qw(stock incorrection found insum back outcorrection disposed
34035b33 Martin Helmling
missing shipped used outsum consumed averconsumed);
}

sub action_usage {
my ($self) = @_;

$main::lxdebug->enter_sub();

my $form = $main::form;
my %myconfig = %main::myconfig;
my $locale = $main::locale;

$form->{title} = t8('UsageE');
$form->{report_generator_output_format} = 'HTML' if !$form->{report_generator_output_format};

my $report = SL::ReportGenerator->new(\%myconfig, $form);

my @columns = qw(partnumber partdescription);

push @columns , qw(ptype unit) if $form->{report_generator_output_format} eq 'HTML';

70edaa5f Geoffrey Richardson
my @numcolumns = qw(stock incorrection found insum back outcorrection disposed
34035b33 Martin Helmling
missing shipped used outsum consumed averconsumed);

push @columns , $self->getnumcolumns();

70edaa5f Geoffrey Richardson
my @hidden_variables = qw(reporttype year duetyp fromdate todate
34035b33 Martin Helmling
warehouse_id bin_id partnumber description bestbefore chargenumber partstypes_id);
my %column_defs = (
'partnumber' => { 'text' => $locale->text('Part Number'), },
'partdescription' => { 'text' => $locale->text('Part_br_Description'), },
'unit' => { 'text' => $locale->text('Unit'), },
'stock' => { 'text' => $locale->text('stock_br'), },
'incorrection' => { 'text' => $locale->text('correction_br'), },
'found' => { 'text' => $locale->text('found_br'), },
'insum' => { 'text' => $locale->text('sum'), },
'back' => { 'text' => $locale->text('back_br'), },
'outcorrection' => { 'text' => $locale->text('correction_br'), },
'disposed' => { 'text' => $locale->text('disposed_br'), },
'missing' => { 'text' => $locale->text('missing_br'), },
'shipped' => { 'text' => $locale->text('shipped_br'), },
'used' => { 'text' => $locale->text('used_br'), },
'outsum' => { 'text' => $locale->text('sum'), },
'consumed' => { 'text' => $locale->text('consumed'), },
'averconsumed' => { 'text' => $locale->text('averconsumed_br'), },
);


map { $column_defs{$_}->{visible} = 1 } @columns;
#map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns;
map { $column_defs{$_}->{align} = 'right' } @numcolumns;

my @custom_headers = ();
# Zeile 1:
push @custom_headers, [
70edaa5f Geoffrey Richardson
{ 'text' => $locale->text('Part'),
34035b33 Martin Helmling
'colspan' => ($form->{report_generator_output_format} eq 'HTML'?4:2), 'align' => 'center'},
{ 'text' => $locale->text('Into bin'), 'colspan' => 4, 'align' => 'center'},
{ 'text' => $locale->text('From bin'), 'colspan' => 7, 'align' => 'center'},
{ 'text' => $locale->text('UsageWithout'), 'colspan' => 2, 'align' => 'center'},
];

# Zeile 2:
my @line_2 = ();
map { push @line_2 , $column_defs{$_} } @columns;
push @custom_headers, [ @line_2 ];

$report->set_custom_headers(@custom_headers);
$report->set_columns( %column_defs );
$report->set_column_order(@columns);

$report->set_export_options('usage', @hidden_variables );

$report->set_sort_indicator($form->{sort}, $form->{order});
$report->set_options('output_format' => 'HTML',
'controller_class' => 'Inventory',
'title' => $form->{title},
# 'html_template' => 'inventory/usage_report',
'attachment_basename' => strftime($locale->text('warehouse_usage_list') . '_%Y%m%d', localtime time));
$report->set_options_from_form;

my %searchparams ;
# form vars
# reporttype = custom
# year = 2014
# duetyp = 7

my $start = DateTime->now_local;
my $end = DateTime->now_local;
my $actualepoch = $end->epoch();
my $days = 365;
my $mdays=30;
$searchparams{reporttype} = $form->{reporttype};
if ($form->{reporttype} eq "custom") {
my $smon = 1;
my $emon = 12;
my $sday = 1;
my $eday = 31;
#forgotten the year --> thisyear
if ($form->{year} !~ m/^\d\d\d\d$/) {
$locale->date(\%myconfig, $form->current_date(\%myconfig), 0) =~
/(\d\d\d\d)/;
$form->{year} = $1;
}
my $leapday = ($form->{year} % 4 == 0) ? 1:0;
#yearly report
if ($form->{duetyp} eq "13") {
$days += $leapday;
}

#Quater reports
if ($form->{duetyp} eq "A") {
$emon = 3;
$days = 90 + $leapday;
}
if ($form->{duetyp} eq "B") {
$smon = 4;
$emon = 6;
$eday = 30;
$days = 91;
}
if ($form->{duetyp} eq "C") {
$smon = 7;
$emon = 9;
$eday = 30;
$days = 92;
}
if ($form->{duetyp} eq "D") {
$smon = 10;
$days = 92;
}
#Monthly reports
if ($form->{duetyp} eq "1" || $form->{duetyp} eq "3" || $form->{duetyp} eq "5" ||
$form->{duetyp} eq "7" || $form->{duetyp} eq "8" || $form->{duetyp} eq "10" ||
$form->{duetyp} eq "12") {
$smon = $emon = $form->{duetyp}*1;
$mdays=$days = 31;
}
if ($form->{duetyp} eq "2" || $form->{duetyp} eq "4" || $form->{duetyp} eq "6" ||
$form->{duetyp} eq "9" || $form->{duetyp} eq "11" ) {
$smon = $emon = $form->{duetyp}*1;
$eday = 30;
if ($form->{duetyp} eq "2" ) {
#this works from 1901 to 2099, 1900 and 2100 fail.
$eday = ($form->{year} % 4 == 0) ? 29 : 28;
}
$mdays=$days = $eday;
}
$searchparams{year} = $form->{year};
$searchparams{duetyp} = $form->{duetyp};
$start->set_month($smon);
$start->set_day($sday);
$start->set_year($form->{year}*1);
$end->set_month($emon);
$end->set_day($eday);
$end->set_year($form->{year}*1);
} else {
$searchparams{fromdate} = $form->{fromdate};
$searchparams{todate} = $form->{todate};
# reporttype = free
# fromdate = 01.01.2014
# todate = 31.05.2014
my ($yy, $mm, $dd) = $locale->parse_date(\%myconfig,$form->{fromdate});
$start->set_year($yy);
$start->set_month($mm);
$start->set_day($dd);
($yy, $mm, $dd) = $locale->parse_date(\%myconfig,$form->{todate});
$end->set_year($yy);
$end->set_month($mm);
$end->set_day($dd);
my $dur = $start->delta_md($end);
$days = $dur->delta_months()*30 + $dur->delta_days() ;
}
$start->set_second(0);
$start->set_minute(0);
$start->set_hour(0);
$end->set_second(59);
$end->set_minute(59);
$end->set_hour(23);
70edaa5f Geoffrey Richardson
if ( $end->epoch() > $actualepoch ) {
34035b33 Martin Helmling
$end = DateTime->now_local;
my $dur = $start->delta_md($end);
$days = $dur->delta_months()*30 + $dur->delta_days() ;
}
if ( $start->epoch() > $end->epoch() ) { $start = $end;$days = 1;}
$days = $mdays if $days < $mdays;
#$main::lxdebug->message(LXDebug->DEBUG2(), "start=".$start->epoch());
#$main::lxdebug->message(LXDebug->DEBUG2(), " end=".$end->epoch());
#$main::lxdebug->message(LXDebug->DEBUG2(), " days=".$days);
my @andfilter = (shippingdate => { ge => $start }, shippingdate => { le => $end } );
if ( $form->{warehouse_id} ) {
push @andfilter , ( warehouse_id => $form->{warehouse_id});
$searchparams{warehouse_id} = $form->{warehouse_id};
if ( $form->{bin_id} ) {
push @andfilter , ( bin_id => $form->{bin_id});
$searchparams{bin_id} = $form->{bin_id};
}
}
# alias class t2 entspricht parts
if ( $form->{partnumber} ) {
push @andfilter , ( 't2.partnumber' => { ilike => '%'. $form->{partnumber} .'%' });
$searchparams{partnumber} = $form->{partnumber};
}
if ( $form->{description} ) {
push @andfilter , ( 't2.description' => { ilike => '%'. $form->{description} .'%' });
$searchparams{description} = $form->{description};
}
if ( $form->{bestbefore} ) {
push @andfilter , ( bestbefore => { eq => $form->{bestbefore} });
$searchparams{bestbefore} = $form->{bestbefore};
}
if ( $form->{chargenumber} ) {
push @andfilter , ( chargenumber => { ilike => '%'.$form->{chargenumber}.'%' });
$searchparams{chargenumber} = $form->{chargenumber};
}
if ( $form->{partstypes_id} ) {
push @andfilter , ( 't2.partstypes_id' => $form->{partstypes_id} );
$searchparams{partstypes_id} = $form->{partstypes_id};
}

my @filter = (and => [ @andfilter ] );

my $objs = SL::DB::Manager::Inventory->get_all(with_objects => ['parts'], where => [ @filter ] , sort_by => 'parts.partnumber ASC');
#my $objs = SL::DB::Inventory->_get_manager_class->get_all(...);

# manual paginating, yuck
my $page = $::form->{page} || 1;
my $pages = {};
$pages->{per_page} = $::form->{per_page} || 20;
my $first_nr = ($page - 1) * $pages->{per_page};
my $last_nr = $first_nr + $pages->{per_page};

my $last_partid = 0;
my $last_row = { };
my $row_ind = 0;
my $allrows = 0;
$allrows = 1 if $form->{report_generator_output_format} ne 'HTML' ;
#$main::lxdebug->message(LXDebug->DEBUG2(), "first_nr=".$first_nr." last_nr=".$last_nr);
foreach my $entry (@{ $objs } ) {
if ( $entry->parts_id != $last_partid ) {
if ( $last_partid > 0 ) {
if ( $allrows || ($row_ind >= $first_nr && $row_ind < $last_nr )) {
$self->make_row_result($last_row,$days,$last_partid);
$report->add_data($last_row);
}
$row_ind++ ;
70edaa5f Geoffrey Richardson
}
34035b33 Martin Helmling
$last_partid = $entry->parts_id;
$last_row = { };
$last_row->{partnumber}->{data} = $entry->part->partnumber;
$last_row->{partdescription}->{data} = $entry->part->description;
$last_row->{unit}->{data} = $entry->part->unit;
$last_row->{stock}->{data} = 0;
$last_row->{incorrection}->{data} = 0;
$last_row->{found}->{data} = 0;
$last_row->{back}->{data} = 0;
$last_row->{outcorrection}->{data} = 0;
$last_row->{disposed}->{data} = 0;
$last_row->{missing}->{data} = 0;
$last_row->{shipped}->{data} = 0;
$last_row->{used}->{data} = 0;
$last_row->{insum}->{data} = 0;
$last_row->{outsum}->{data} = 0;
$last_row->{consumed}->{data} = 0;
$last_row->{averconsumed}->{data} = 0;
}
if ( !$allrows && $row_ind >= $last_nr ) {
next;
}
my $prefix='';
if ( $entry->trans_type->description eq 'correction' ) {
$prefix = $entry->trans_type->direction;
}
70edaa5f Geoffrey Richardson
$last_row->{$prefix.$entry->trans_type->description}->{data} +=
34035b33 Martin Helmling
( $entry->trans_type->direction eq 'out' ? -$entry->qty : $entry->qty );
}
if ( $last_partid > 0 && ( $allrows || ($row_ind >= $first_nr && $row_ind < $last_nr ))) {
$self->make_row_result($last_row,$days,$last_partid);
$report->add_data($last_row);
$row_ind++ ;
70edaa5f Geoffrey Richardson
}
34035b33 Martin Helmling
my $num_rows = @{ $report->{data} } ;
#$main::lxdebug->message(LXDebug->DEBUG2(), "count=".$row_ind." rows=".$num_rows);

if ( ! $allrows ) {
$pages->{max} = SL::DB::Helper::Paginated::ceil($row_ind, $pages->{per_page}) || 1;
$pages->{page} = $page < 1 ? 1: $page > $pages->{max} ? $pages->{max}: $page;
$pages->{common} = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{page}, $pages->{max}) } ];
$self->{pages} = $pages;
$searchparams{action} = "usage";
$self->{base_url} = $self->url_for(\%searchparams );
#$main::lxdebug->message(LXDebug->DEBUG2(), "page=".$pages->{page}." url=".$self->{base_url});

$report->set_options('raw_bottom_info_text' => $self->render('inventory/report_bottom', { output => 0 }) );
}
e7913c4c Moritz Bunkus
$report->generate_with_headers();
34035b33 Martin Helmling
$main::lxdebug->leave_sub();

}

sub make_row_result {
my ($self,$row,$days,$partid) = @_;
my $form = $main::form;
my $myconfig = \%main::myconfig;

$row->{insum}->{data} = $row->{stock}->{data} + $row->{incorrection}->{data} + $row->{found}->{data};
$row->{outsum}->{data} = $row->{back}->{data} + $row->{outcorrection}->{data} + $row->{disposed}->{data} +
$row->{missing}->{data} + $row->{shipped}->{data} + $row->{used}->{data};
70edaa5f Geoffrey Richardson
$row->{consumed}->{data} = $row->{outsum}->{data} -
34035b33 Martin Helmling
$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();
12f8fb50 Geoffrey Richardson
$row->{partnumber}->{link} = 'controller.pl?action=Part/edit&part.id' . $partid;
34035b33 Martin Helmling
}

666d4cad Sven Schöling
sub action_stock {
my ($self) = @_;

81b7704d Bernd Bleßmann
my $transfer_error;
8209ac91 Sven Schöling
my $qty = $::form->parse_amount(\%::myconfig, $::form->{qty});
74a70e2a Sven Schöling
if (!$qty) {
81b7704d Bernd Bleßmann
$transfer_error = t8('Cannot stock without amount');
74a70e2a Sven Schöling
} elsif ($qty < 0) {
81b7704d Bernd Bleßmann
$transfer_error = t8('Cannot stock negative amounts');
8209ac91 Sven Schöling
} else {
# do stock
81b7704d Bernd Bleßmann
$::form->throw_on_error(sub {
eval {
WH->transfer({
parts => $self->part,
dst_bin => $self->bin,
dst_wh => $self->warehouse,
qty => $qty,
unit => $self->unit,
transfer_type => 'stock',
chargenumber => $::form->{chargenumber},
bestbefore => $::form->{bestbefore},
ean => $::form->{ean},
comment => $::form->{comment},
});
1;
} or do { $transfer_error = $EVAL_ERROR->getMessage; }
8209ac91 Sven Schöling
});
666d4cad Sven Schöling
81b7704d Bernd Bleßmann
if (!$transfer_error) {
if ($::form->{write_default_bin}) {
$self->part->load; # onhand is calculated in between. don't mess that up
$self->part->bin($self->bin);
$self->part->warehouse($self->warehouse);
$self->part->save;
}

flash_later('info', t8('Transfer successful'));
8209ac91 Sven Schöling
}
81b7704d Bernd Bleßmann
}
8209ac91 Sven Schöling
81b7704d Bernd Bleßmann
my %additional_redirect_params = ();
if ($transfer_error) {
flash_later('error', $transfer_error);
$additional_redirect_params{$_} = $::form->{$_} for qw(qty chargenumber bestbefore ean comment);
$additional_redirect_params{qty} = $qty;
8209ac91 Sven Schöling
}
666d4cad Sven Schöling
# redirect
$self->redirect_to(
action => 'stock_in',
part_id => $self->part->id,
bin_id => $self->bin->id,
warehouse_id => $self->warehouse->id,
5491c513 Sven Schöling
unit_id => $self->unit->id,
81b7704d Bernd Bleßmann
%additional_redirect_params,
666d4cad Sven Schöling
);
}

sub action_part_changed {
my ($self) = @_;

# no standard? ask user if he wants to write it
if ($self->part->id && !$self->part->bin_id && !$self->part->warehouse_id) {
$self->js->show('#write_default_bin_span');
} else {
$self->js->hide('#write_default_bin_span')
->removeAttr('#write_default_bin', 'checked');
}

$self->js
->replaceWith('#warehouse_id', $self->build_warehouse_select)
->replaceWith('#bin_id', $self->build_bin_select)
->replaceWith('#unit_id', $self->build_unit_select)
->focus('#warehouse_id')
fa415e21 Sven Schöling
->render;
666d4cad Sven Schöling
}

sub action_warehouse_changed {
my ($self) = @_;

$self->js
->replaceWith('#bin_id', $self->build_bin_select)
->focus('#bin_id')
fa415e21 Sven Schöling
->render;
666d4cad Sven Schöling
}

sub action_mini_stock {
my ($self) = @_;

$self->js
5f543a5e Sven Schöling
->html('#stock', $self->render('inventory/_stock', { output => 0 }))
fa415e21 Sven Schöling
->render;
666d4cad Sven Schöling
}

51072516 Bernd Bleßmann
sub action_stocktaking {
my ($self) = @_;

$::request->{layout}->use_javascript("${_}.js") for qw(kivi.Inventory);
$::request->layout->focus('#part_id_name');
$self->setup_stock_stocktaking_action_bar;
$self->render('inventory/stocktaking/form', title => t8('Stocktaking'));
}

sub action_save_stocktaking {
my ($self) = @_;

return $self->js->flash('error', t8('A target quantitiy has to be given'))->render()
if $::form->{target_qty} eq '';

my $target_qty = $::form->parse_amount(\%::myconfig, $::form->{target_qty});

return $self->js->flash('error', t8('Error: A negative target quantity is not allowed.'))->render()
if $target_qty < 0;

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);

if (!$::form->{dont_check_already_counted}) {
my $already_counted = _already_counted($self->part,
warehouse_id => $self->warehouse->id,
bin_id => $self->bin->id,
cutoff_date => $::form->{cutoff_date_as_date},
chargenumber => $::form->{chargenumber},
bestbefore => $::form->{bestbefore});
if (scalar @$already_counted) {
my $reply = $self->js->dialog->open({
html => $self->render('inventory/stocktaking/_already_counted_dialog',
{ output => 0 },
already_counted => $already_counted,
stocked_qty => $stocked_qty,
stocked_qty_in_form_units => $stocked_qty_in_form_units),
id => 'already_counted_dialog',
dialog => {
title => t8('Already counted'),
},
})->render;

return $reply;
}
}

# - target_qty is in units given in form ($self->unit)
# - WH->transfer expects qtys in given unit (here: unit from form (unit -> $self->unit))
# Therefore use stocked_qty in form units for calculation.
my $qty = $target_qty - $stocked_qty_in_form_units;
my $src_or_dst = $qty < 0? 'src' : 'dst';
$qty = abs($qty);

my $transfer_error;
# do stock
$::form->throw_on_error(sub {
eval {
WH->transfer({
parts => $self->part,
$src_or_dst.'_bin' => $self->bin,
$src_or_dst.'_wh' => $self->warehouse,
qty => $qty,
unit => $self->unit,
transfer_type => 'stocktaking',
chargenumber => $::form->{chargenumber},
bestbefore => $::form->{bestbefore},
ean => $::form->{ean},
comment => $::form->{comment},
record_stocktaking => 1,
stocktaking_qty => $target_qty,
stocktaking_cutoff_date => $::form->{cutoff_date_as_date},
});
1;
} or do { $transfer_error = $EVAL_ERROR->getMessage; }
});

return $self->js->flash('error', $transfer_error)->render()
if $transfer_error;

flash_later('info', $::locale->text('Part successful counted'));
$self->redirect_to(action => 'stocktaking',
warehouse_id => $self->warehouse->id,
bin_id => $self->bin->id,
cutoff_date_as_date => $self->stocktaking_cutoff_date->to_kivitendo);
}

sub action_reload_stocktaking_history {
my ($self) = @_;

$::form->{filter}{'cutoff_date:date'} = $self->stocktaking_cutoff_date->to_kivitendo;
$::form->{filter}{'employee_id'} = SL::DB::Manager::Employee->current->id;

$self->prepare_stocktaking_report;
$self->report_generator_list_objects(report => $self->{report}, objects => $self->stocktaking_models->get, layout => 0, header => 0);
}

sub action_stocktaking_part_changed {
my ($self) = @_;

$self->js
->replaceWith('#unit_id', $self->build_unit_select)
->focus('#target_qty')
->render;
}

sub action_stocktaking_journal {
my ($self) = @_;

$self->prepare_stocktaking_report(full => 1);
$self->report_generator_list_objects(report => $self->{report}, objects => $self->stocktaking_models->get);
}
666d4cad Sven Schöling
#================================================================

sub _check_auth {
$main::auth->assert('warehouse_management');
}

sub _check_warehouses {
$_[0]->show_no_warehouses_error if !@{ $_[0]->warehouses };
}

sub init_warehouses {
44655653 Sven Schöling
SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]]);
666d4cad Sven Schöling
}

34035b33 Martin Helmling
#sub init_bins {
# SL::DB::Manager::Bin->get_all();
#}

666d4cad Sven Schöling
sub init_units {
SL::DB::Manager::Unit->get_all;
}

51072516 Bernd Bleßmann
sub init_is_stocktaking {
return $_[0]->action_name =~ m{stocktaking};
}

sub init_stocktaking_models {
my ($self) = @_;

SL::Controller::Helper::GetModels->new(
controller => $self,
model => 'Stocktaking',
sorted => {
_default => {
by => 'itime',
dir => 0,
},
itime => t8('Insert Date'),
qty => t8('Target Qty'),
chargenumber => t8('Charge Number'),
comment => t8('Comment'),
employee => t8('Employee'),
ean => t8('EAN'),
partnumber => t8('Part Number'),
part => t8('Part Description'),
bin => t8('Bin'),
cutoff_date => t8('Cutoff Date'),
},
with_objects => ['employee', 'parts', 'warehouse', 'bin'],
);
}

sub init_stocktaking_cutoff_date {
my ($self) = @_;

return DateTime->from_kivitendo($::form->{cutoff_date_as_date}) if $::form->{cutoff_date_as_date};
return SL::DB::Default->get->stocktaking_cutoff_date if SL::DB::Default->get->stocktaking_cutoff_date;

# Default cutoff date is last day of current year, but if current month
# is janurary, it is the last day of the last year.
my $now = DateTime->now_local;
my $cutoff = DateTime->new(year => $now->year, month => 12, day => 31);
if ($now->month < 1) {
$cutoff->substract(years => 1);
}
return $cutoff;
}

666d4cad Sven Schöling
sub set_target_from_part {
my ($self) = @_;

return if !$self->part;

$self->warehouse($self->part->warehouse) if $self->part->warehouse;
$self->bin( $self->part->bin) if $self->part->bin;
}

sub sanitize_target {
my ($self) = @_;

44655653 Sven Schöling
$self->warehouse($self->warehouses->[0]) if !$self->warehouse || !$self->warehouse->id;
$self->bin ($self->warehouse->bins->[0]) if !$self->bin || !$self->bin->id;
34035b33 Martin Helmling
# foreach my $warehouse ( $self->warehouses ) {
# $warehouse->{BINS} = [];
# foreach my $bin ( $self->bins ) {
# if ( $bin->warehouse_id == $warehouse->id ) {
# push @{ $warehouse->{BINS} }, $bin;
# }
# }
# }
666d4cad Sven Schöling
}

sub load_part_from_form {
$_[0]->part(SL::DB::Manager::Part->find_by_or_create(id => $::form->{part_id}));
}

sub load_unit_from_form {
$_[0]->unit(SL::DB::Manager::Unit->find_by_or_create(id => $::form->{unit_id}));
}

sub load_wh_from_form {
51072516 Bernd Bleßmann
my $preselected;
$preselected = SL::DB::Default->get->stocktaking_warehouse_id if $_[0]->is_stocktaking;

$_[0]->warehouse(SL::DB::Manager::Warehouse->find_by_or_create(id => ($::form->{warehouse_id} || $preselected)));
666d4cad Sven Schöling
}

sub load_bin_from_form {
51072516 Bernd Bleßmann
my $preselected;
$preselected = SL::DB::Default->get->stocktaking_bin_id if $_[0]->is_stocktaking;

$_[0]->bin(SL::DB::Manager::Bin->find_by_or_create(id => ($::form->{bin_id} || $preselected)));
666d4cad Sven Schöling
}

sub set_layout {
$::request->layout->add_javascripts('client_js.js');
}

sub build_warehouse_select {
a97574b8 Sven Schöling
select_tag('warehouse_id', $_[0]->warehouses,
666d4cad Sven Schöling
title_key => 'description',
default => $_[0]->warehouse->id,
onchange => 'reload_bin_selection()',
)
}

sub build_bin_select {
a97574b8 Sven Schöling
select_tag('bin_id', [ $_[0]->warehouse->bins ],
666d4cad Sven Schöling
title_key => 'description',
default => $_[0]->bin->id,
);
}

sub build_unit_select {
$_[0]->part->id
a97574b8 Sven Schöling
? select_tag('unit_id', $_[0]->part->available_units,
666d4cad Sven Schöling
title_key => 'name',
default => $_[0]->part->unit_obj->id,
)
a97574b8 Sven Schöling
: select_tag('unit_id', $_[0]->units,
666d4cad Sven Schöling
title_key => 'name',
)
}

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);

6f3f13dd Sven Schöling
my $objs;
ce3d9171 Sven Schöling
$objs = SL::DB::Manager::Inventory->get_all(query => [ trans_id => \@ids ]) if @ids;
666d4cad Sven Schöling
# at most 2 of them belong to a transaction and the qty determins in or out.
# sort them for display
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;

return \@sorted;
}

5f543a5e Sven Schöling
sub mini_stock {
my ($self) = @_;

my $stock = $self->part->get_simple_stock;
$self->{stock_by_bin} = { map { $_->{bin_id} => $_ } @$stock };
$self->{stock_empty} = ! grep { $_->{sum} * 1 } @$stock;
}

039226c3 Sven Schöling
sub show_no_warehouses_error {
666d4cad Sven Schöling
my ($self) = @_;

my $msg = t8('No warehouse has been created yet or the quantity of the bins is not configured yet.') . ' ';

4bd1e2f8 Sven Schöling
if ($::auth->check_right($::myconfig{login}, 'config')) { # TODO wut?
666d4cad Sven Schöling
$msg .= t8('You can create warehouses and bins via the menu "System -> Warehouses".');
} else {
$msg .= t8('Please ask your administrator to create warehouses and bins.');
}
$::form->show_generic_error($msg);
}

51072516 Bernd Bleßmann
sub prepare_stocktaking_report {
my ($self, %params) = @_;

my $callback = $self->stocktaking_models->get_callback;

my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
$self->{report} = $report;

my @columns = qw(itime employee ean partnumber part qty unit bin chargenumber comment cutoff_date);
my @sortable = qw(itime employee ean partnumber part qty bin chargenumber comment cutoff_date);

my %column_defs = (
itime => { sub => sub { $_[0]->itime_as_timestamp },
text => t8('Insert Date'), },
employee => { sub => sub { $_[0]->employee->safe_name },
text => t8('Employee'), },
ean => { sub => sub { $_[0]->part->ean },
text => t8('EAN'), },
partnumber => { sub => sub { $_[0]->part->partnumber },
text => t8('Part Number'), },
part => { sub => sub { $_[0]->part->description },
text => t8('Part Description'), },
qty => { sub => sub { $_[0]->qty_as_number },
text => t8('Target Qty'),
align => 'right', },
unit => { sub => sub { $_[0]->part->unit },
text => t8('Unit'), },
bin => { sub => sub { $_[0]->bin->full_description },
text => t8('Bin'), },
chargenumber => { text => t8('Charge Number'), },
comment => { text => t8('Comment'), },
cutoff_date => { sub => sub { $_[0]->cutoff_date_as_date },
text => t8('Cutoff Date'), },
);

$report->set_options(
std_column_visibility => 1,
controller_class => 'Inventory',
output_format => 'HTML',
title => (!!$params{full})? $::locale->text('Stocktaking Journal') : $::locale->text('Stocktaking History'),
allow_pdf_export => !!$params{full},
allow_csv_export => !!$params{full},
);
$report->set_columns(%column_defs);
$report->set_column_order(@columns);
$report->set_export_options(qw(stocktaking_journal filter));
$report->set_options_from_form;
$self->stocktaking_models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
$self->stocktaking_models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable) if !!$params{full};
if (!!$params{full}) {
$report->set_options(
raw_top_info_text => $self->render('inventory/stocktaking/full_report_top', { output => 0 }),
);
}
$report->set_options(
raw_bottom_info_text => $self->render('inventory/stocktaking/report_bottom', { output => 0 }),
);
}

sub _get_stocked_qty {
my ($part, %params) = @_;

my $bestbefore_filter = '';
my $bestbefore_val_cnt = 0;
if ($::instance_conf->get_show_bestbefore) {
$bestbefore_filter = ($params{bestbefore}) ? 'AND bestbefore = ?' : 'AND bestbefore IS NULL';
$bestbefore_val_cnt = ($params{bestbefore}) ? 1 : 0;
}

my $query = <<SQL;
SELECT sum(qty) FROM inventory
WHERE parts_id = ? AND warehouse_id = ? AND bin_id = ? AND chargenumber = ? $bestbefore_filter
GROUP BY warehouse_id, bin_id, chargenumber
SQL

my @values = ($part->id,
$params{warehouse_id},
$params{bin_id},
$params{chargenumber});
push @values, $params{bestbefore} if $bestbefore_val_cnt;

my ($stocked_qty) = selectrow_query($::form, $::form->get_standard_dbh, $query, @values);

return 1*($stocked_qty || 0);
}

sub _already_counted {
my ($part, %params) = @_;

my %bestbefore_filter;
if ($::instance_conf->get_show_bestbefore) {
%bestbefore_filter = (bestbefore => $params{bestbefore});
}

SL::DB::Manager::Stocktaking->get_all(query => [and => [parts_id => $part->id,
warehouse_id => $params{warehouse_id},
bin_id => $params{bin_id},
cutoff_date => $params{cutoff_date},
chargenumber => $params{chargenumber},
%bestbefore_filter]],
sort_by => ['itime DESC']);
}

fedfc383 Moritz Bunkus
sub setup_stock_in_action_bar {
my ($self, %params) = @_;

for my $bar ($::request->layout->get('actionbar')) {
$bar->add(
action => [
t8('Stock'),
submit => [ '#form', { action => 'Inventory/stock' } ],
checks => [ 'check_part_selection_before_stocking' ],
accesskey => 'enter',
],
);
}
}

sub setup_stock_usage_action_bar {
my ($self, %params) = @_;

for my $bar ($::request->layout->get('actionbar')) {
$bar->add(
action => [
t8('Show'),
submit => [ '#form', { action => 'Inventory/usage' } ],
accesskey => 'enter',
],
);
}
}

51072516 Bernd Bleßmann
sub setup_stock_stocktaking_action_bar {
my ($self, %params) = @_;

for my $bar ($::request->layout->get('actionbar')) {
$bar->add(
action => [
t8('Save'),
call => [ 'kivi.Inventory.save_stocktaking' ],
accesskey => 'enter',
],
);
}
}

666d4cad Sven Schöling
1;
8cdadf55 Martin Helmling
__END__

=encoding utf-8

=head1 NAME

51072516 Bernd Bleßmann
SL::Controller::Inventory - Controller for inventory
8cdadf55 Martin Helmling
=head1 DESCRIPTION

51072516 Bernd Bleßmann
This controller handles stock in, stocktaking and reports about inventory
in warehouses/stocks
8cdadf55 Martin Helmling
- warehouse content

- warehouse journal

- warehouse withdrawal

51072516 Bernd Bleßmann
- stocktaking

=head2 Stocktaking

Stocktaking allows to document the counted quantities of parts during
stocktaking for a certain cutoff date. Differences between counted and stocked
quantities are corrected in the stock. The transfer type 'stocktacking' is set
here.

After picking a part, the mini stock for this part is displayed. At the bottom
of the form a history of already counted parts for the current employee and the
choosen cutoff date is shown.

Warehouse, bin and cutoff date canbe preselected in the client configuration.

If a part was already counted for this cutoff date, warehouse and bin, a warning
is displayed, allowing the user to choose to add the counted quantity to the
stocked one or to take his counted quantity as the new stocked quantity.

There is also a journal of stocktakings.

Templates are located under C<templates/webpages/inventory/stocktaking>.
JavaScript functions can be found in C<js/kivi.Inventory.js>.

8cdadf55 Martin Helmling
=head1 FUNCTIONS

=over 4

=item C<action_stock_usage>

Create a search form for stock withdrawal.
The search parameter for report are made like the reports in bin/mozilla/rp.pl

=item C<action_usage>

Make a report about stock withdrawal.

The manual pagination is implemented like the pagination in SL::Controller::CsvImport.

51072516 Bernd Bleßmann
=item C<action_stocktaking>

This action renders the input form for stocktaking.

=item C<action_save_stocktaking>

This action saves the stocktaking values and corrects the stock after checking
if the part is already counted for this warehouse, bin and cutoff date.
For saving SL::WH->transfer is called.

=item C<action_reload_stocktaking_history>

This action is responsible for displaying the stocktaking history at the bottom
of the form. It uses the stocktaking journal with fixed filters for cutoff date
and the current employee. The history is displayed via javascript.

=item C<action_stocktaking_part_changed>

This action is called after the user selected or changed the part.

=item C<is_stocktaking>

This is a method to check if actions are called from stocktaking form.

8cdadf55 Martin Helmling
=back

=head1 SPECIAL CASES

Because of the PFD-Table Formatter some parameters for PDF must be different to the HTML parameters.
So in german language there are some tries to use a HTML Break in the second heading line
to produce two line heading inside table. The actual version has some abbreviations for the header texts.

=head1 BUGS

The PDF-Table library has some limits (doesn't display all if the line is to large) so
the format is adapted to this


=head1 AUTHOR

51072516 Bernd Bleßmann
=over 4

=item only for C<action_stock_usage> and C<action_usage>:
8cdadf55 Martin Helmling
4825c38f Bernd Bleßmann
Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
8cdadf55 Martin Helmling
51072516 Bernd Bleßmann
=item for stocktaking:

Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>

=back
8cdadf55 Martin Helmling
=cut