1 package SL::Controller::Inventory;
 
   5 use POSIX qw(strftime);
 
   7 use parent qw(SL::Controller::Base);
 
  10 use SL::DB::Stocktaking;
 
  12 use SL::DB::Warehouse;
 
  16 use SL::ReportGenerator;
 
  17 use SL::Locale::String qw(t8);
 
  18 use SL::Presenter::Tag qw(select_tag);
 
  20 use SL::Helper::Flash;
 
  21 use SL::Controller::Helper::ReportGenerator;
 
  22 use SL::Controller::Helper::GetModels;
 
  24 use English qw(-no_match_vars);
 
  26 use Rose::Object::MakeMethods::Generic (
 
  27   'scalar --get_set_init' => [ qw(warehouses units is_stocktaking stocktaking_models stocktaking_cutoff_date) ],
 
  28   'scalar'                => [ qw(warehouse bin unit part) ],
 
  31 __PACKAGE__->run_before('_check_auth');
 
  32 __PACKAGE__->run_before('_check_warehouses');
 
  33 __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) ]);
 
  34 __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) ]);
 
  35 __PACKAGE__->run_before('load_wh_from_form',     only => [ qw(stock_in warehouse_changed stock stocktaking stocktaking_get_warn_qty_threshold save_stocktaking) ]);
 
  36 __PACKAGE__->run_before('load_bin_from_form',    only => [ qw(stock_in stock stocktaking stocktaking_get_warn_qty_threshold save_stocktaking) ]);
 
  37 __PACKAGE__->run_before('set_target_from_part',  only => [ qw(part_changed) ]);
 
  38 __PACKAGE__->run_before('mini_stock',            only => [ qw(stock_in mini_stock) ]);
 
  39 __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) ]);
 
  40 __PACKAGE__->run_before('set_layout');
 
  45   $::form->{title}   = t8('Stock');
 
  47   $::request->layout->focus('#part_id_name');
 
  48   my $transfer_types = WH->retrieve_transfer_types('in');
 
  49   map { $_->{description} = $main::locale->text($_->{description}) } @{ $transfer_types };
 
  50   $self->setup_stock_in_action_bar;
 
  51   $self->render('inventory/warehouse_selection_stock', title => $::form->{title}, TRANSFER_TYPES => $transfer_types );
 
  54 sub action_stock_usage {
 
  57   $::form->{title}   = t8('UsageE');
 
  59   $::form->get_lists('warehouses' => { 'key'    => 'WAREHOUSES',
 
  62   $self->setup_stock_usage_action_bar;
 
  63   $self->render('inventory/warehouse_usage',
 
  64                 title => $::form->{title},
 
  65                 year => DateTime->today->year,
 
  66                 WAREHOUSES => $::form->{WAREHOUSES},
 
  67                 WAREHOUSE_FILTER => 1,
 
  76   return qw(stock incorrection found insum back outcorrection disposed
 
  77                      missing shipped used outsum consumed averconsumed);
 
  83   $main::lxdebug->enter_sub();
 
  85   my $form     = $main::form;
 
  86   my %myconfig = %main::myconfig;
 
  87   my $locale   = $main::locale;
 
  89   $form->{title}   = t8('UsageE');
 
  90   $form->{report_generator_output_format} = 'HTML' if !$form->{report_generator_output_format};
 
  92   my $report = SL::ReportGenerator->new(\%myconfig, $form);
 
  94   my @columns = qw(partnumber partdescription);
 
  96   push @columns , qw(ptype unit) if $form->{report_generator_output_format} eq 'HTML';
 
  98   my @numcolumns = qw(stock incorrection found insum back outcorrection disposed
 
  99                      missing shipped used outsum consumed averconsumed);
 
 101   push @columns , $self->getnumcolumns();
 
 103   my @hidden_variables = qw(reporttype year duetyp fromdate todate
 
 104                             warehouse_id bin_id partnumber description bestbefore chargenumber partstypes_id);
 
 106     'partnumber'      => { 'text' => $locale->text('Part Number'), },
 
 107     'partdescription' => { 'text' => $locale->text('Part_br_Description'), },
 
 108     'unit'            => { 'text' => $locale->text('Unit'), },
 
 109     'stock'           => { 'text' => $locale->text('stock_br'), },
 
 110     'incorrection'    => { 'text' => $locale->text('correction_br'), },
 
 111     'found'           => { 'text' => $locale->text('found_br'), },
 
 112     'insum'           => { 'text' => $locale->text('sum'), },
 
 113     'back'            => { 'text' => $locale->text('back_br'), },
 
 114     'outcorrection'   => { 'text' => $locale->text('correction_br'), },
 
 115     'disposed'        => { 'text' => $locale->text('disposed_br'), },
 
 116     'missing'         => { 'text' => $locale->text('missing_br'), },
 
 117     'shipped'         => { 'text' => $locale->text('shipped_br'), },
 
 118     'used'            => { 'text' => $locale->text('used_br'), },
 
 119     'outsum'          => { 'text' => $locale->text('sum'), },
 
 120     'consumed'        => { 'text' => $locale->text('consumed'), },
 
 121     'averconsumed'    => { 'text' => $locale->text('averconsumed_br'), },
 
 125   map { $column_defs{$_}->{visible} = 1 } @columns;
 
 126   #map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns;
 
 127   map { $column_defs{$_}->{align} = 'right' } @numcolumns;
 
 129   my @custom_headers = ();
 
 131   push @custom_headers, [
 
 132       { 'text' => $locale->text('Part'),
 
 133         'colspan' => ($form->{report_generator_output_format} eq 'HTML'?4:2), 'align' => 'center'},
 
 134       { 'text' => $locale->text('Into bin'), 'colspan' => 4, 'align' => 'center'},
 
 135       { 'text' => $locale->text('From bin'), 'colspan' => 7, 'align' => 'center'},
 
 136       { 'text' => $locale->text('UsageWithout'),    'colspan' => 2, 'align' => 'center'},
 
 141   map { push @line_2 , $column_defs{$_} } @columns;
 
 142   push @custom_headers, [ @line_2 ];
 
 144   $report->set_custom_headers(@custom_headers);
 
 145   $report->set_columns( %column_defs );
 
 146   $report->set_column_order(@columns);
 
 148   $report->set_export_options('usage', @hidden_variables );
 
 150   $report->set_sort_indicator($form->{sort}, $form->{order});
 
 151   $report->set_options('output_format'        => 'HTML',
 
 152                        'controller_class'     => 'Inventory',
 
 153                        'title'                => $form->{title},
 
 154 #                      'html_template'        => 'inventory/usage_report',
 
 155                        'attachment_basename'  => strftime($locale->text('warehouse_usage_list') . '_%Y%m%d', localtime time));
 
 156   $report->set_options_from_form;
 
 160 #   reporttype = custom
 
 164   my $start       = DateTime->now_local;
 
 165   my $end         = DateTime->now_local;
 
 166   my $actualepoch = $end->epoch();
 
 169   $searchparams{reporttype} = $form->{reporttype};
 
 170   if ($form->{reporttype} eq "custom") {
 
 175     #forgotten the year --> thisyear
 
 176     if ($form->{year} !~ m/^\d\d\d\d$/) {
 
 177       $locale->date(\%myconfig, $form->current_date(\%myconfig), 0) =~
 
 181     my $leapday = ($form->{year} % 4 == 0) ? 1:0;
 
 183     if ($form->{duetyp} eq "13") {
 
 188     if ($form->{duetyp} eq "A") {
 
 190       $days = 90 + $leapday;
 
 192     if ($form->{duetyp} eq "B") {
 
 198     if ($form->{duetyp} eq "C") {
 
 204     if ($form->{duetyp} eq "D") {
 
 209     if ($form->{duetyp} eq "1" || $form->{duetyp} eq "3" || $form->{duetyp} eq "5" ||
 
 210         $form->{duetyp} eq "7" || $form->{duetyp} eq "8" || $form->{duetyp} eq "10" ||
 
 211         $form->{duetyp} eq "12") {
 
 212         $smon = $emon = $form->{duetyp}*1;
 
 215     if ($form->{duetyp} eq "2" || $form->{duetyp} eq "4" || $form->{duetyp} eq "6" ||
 
 216         $form->{duetyp} eq "9" || $form->{duetyp} eq "11" ) {
 
 217         $smon = $emon = $form->{duetyp}*1;
 
 219         if ($form->{duetyp} eq "2" ) {
 
 220             #this works from 1901 to 2099, 1900 and 2100 fail.
 
 221             $eday = ($form->{year} % 4 == 0) ? 29 : 28;
 
 223         $mdays=$days = $eday;
 
 225     $searchparams{year} = $form->{year};
 
 226     $searchparams{duetyp} = $form->{duetyp};
 
 227     $start->set_month($smon);
 
 228     $start->set_day($sday);
 
 229     $start->set_year($form->{year}*1);
 
 230     $end->set_month($emon);
 
 231     $end->set_day($eday);
 
 232     $end->set_year($form->{year}*1);
 
 234     $searchparams{fromdate} = $form->{fromdate};
 
 235     $searchparams{todate} = $form->{todate};
 
 237 #   fromdate = 01.01.2014
 
 238 #   todate = 31.05.2014
 
 239     my ($yy, $mm, $dd) = $locale->parse_date(\%myconfig,$form->{fromdate});
 
 240     $start->set_year($yy);
 
 241     $start->set_month($mm);
 
 242     $start->set_day($dd);
 
 243     ($yy, $mm, $dd) = $locale->parse_date(\%myconfig,$form->{todate});
 
 245     $end->set_month($mm);
 
 247     my $dur = $start->delta_md($end);
 
 248     $days = $dur->delta_months()*30 + $dur->delta_days() ;
 
 250   $start->set_second(0);
 
 251   $start->set_minute(0);
 
 253   $end->set_second(59);
 
 254   $end->set_minute(59);
 
 256   if ( $end->epoch() > $actualepoch ) {
 
 257       $end = DateTime->now_local;
 
 258       my $dur = $start->delta_md($end);
 
 259       $days = $dur->delta_months()*30 + $dur->delta_days() ;
 
 261   if ( $start->epoch() > $end->epoch() ) { $start = $end;$days = 1;}
 
 262   $days = $mdays if $days < $mdays;
 
 263   #$main::lxdebug->message(LXDebug->DEBUG2(), "start=".$start->epoch());
 
 264   #$main::lxdebug->message(LXDebug->DEBUG2(), "  end=".$end->epoch());
 
 265   #$main::lxdebug->message(LXDebug->DEBUG2(), " days=".$days);
 
 266   my @andfilter = (shippingdate => { ge => $start }, shippingdate => { le => $end } );
 
 267   if ( $form->{warehouse_id} ) {
 
 268       push @andfilter , ( warehouse_id => $form->{warehouse_id});
 
 269       $searchparams{warehouse_id} = $form->{warehouse_id};
 
 270       if ( $form->{bin_id} ) {
 
 271           push @andfilter , ( bin_id => $form->{bin_id});
 
 272           $searchparams{bin_id} = $form->{bin_id};
 
 275   # alias class t2 entspricht parts
 
 276   if ( $form->{partnumber} ) {
 
 277       push @andfilter , ( 't2.partnumber' => { ilike => '%'. $form->{partnumber} .'%' });
 
 278       $searchparams{partnumber} = $form->{partnumber};
 
 280   if ( $form->{description} ) {
 
 281       push @andfilter , ( 't2.description' => { ilike => '%'. $form->{description} .'%'  });
 
 282       $searchparams{description} = $form->{description};
 
 284   if ( $form->{bestbefore} ) {
 
 285     push @andfilter , ( bestbefore => { eq => $form->{bestbefore} });
 
 286       $searchparams{bestbefore} = $form->{bestbefore};
 
 288   if ( $form->{chargenumber} ) {
 
 289       push @andfilter , ( chargenumber => { ilike => '%'.$form->{chargenumber}.'%' });
 
 290       $searchparams{chargenumber} = $form->{chargenumber};
 
 292   if ( $form->{partstypes_id} ) {
 
 293       push @andfilter , ( 't2.partstypes_id' => $form->{partstypes_id} );
 
 294       $searchparams{partstypes_id} = $form->{partstypes_id};
 
 297   my @filter = (and => [ @andfilter ] );
 
 299   my $objs = SL::DB::Manager::Inventory->get_all(with_objects => ['parts'], where => [ @filter ] , sort_by => 'parts.partnumber ASC');
 
 300   #my $objs = SL::DB::Inventory->_get_manager_class->get_all(...);
 
 302   # manual paginating, yuck
 
 303   my $page = $::form->{page} || 1;
 
 305   $pages->{per_page}        = $::form->{per_page} || 20;
 
 306   my $first_nr = ($page - 1) * $pages->{per_page};
 
 307   my $last_nr  = $first_nr + $pages->{per_page};
 
 313   $allrows = 1 if $form->{report_generator_output_format} ne 'HTML' ;
 
 314   #$main::lxdebug->message(LXDebug->DEBUG2(), "first_nr=".$first_nr." last_nr=".$last_nr);
 
 315   foreach my $entry (@{ $objs } ) {
 
 316       if ( $entry->parts_id != $last_partid ) {
 
 317           if ( $last_partid > 0 ) {
 
 318               if ( $allrows || ($row_ind >= $first_nr && $row_ind < $last_nr )) {
 
 319                   $self->make_row_result($last_row,$days,$last_partid);
 
 320                   $report->add_data($last_row);
 
 324           $last_partid = $entry->parts_id;
 
 326           $last_row->{partnumber}->{data} = $entry->part->partnumber;
 
 327           $last_row->{partdescription}->{data} = $entry->part->description;
 
 328           $last_row->{unit}->{data} = $entry->part->unit;
 
 329           $last_row->{stock}->{data} = 0;
 
 330           $last_row->{incorrection}->{data} = 0;
 
 331           $last_row->{found}->{data} = 0;
 
 332           $last_row->{back}->{data} = 0;
 
 333           $last_row->{outcorrection}->{data} = 0;
 
 334           $last_row->{disposed}->{data} = 0;
 
 335           $last_row->{missing}->{data} = 0;
 
 336           $last_row->{shipped}->{data} = 0;
 
 337           $last_row->{used}->{data} = 0;
 
 338           $last_row->{insum}->{data} = 0;
 
 339           $last_row->{outsum}->{data} = 0;
 
 340           $last_row->{consumed}->{data} = 0;
 
 341           $last_row->{averconsumed}->{data} = 0;
 
 343       if ( !$allrows && $row_ind >= $last_nr ) {
 
 347       if ( $entry->trans_type->description eq 'correction' ) {
 
 348           $prefix = $entry->trans_type->direction;
 
 350       $last_row->{$prefix.$entry->trans_type->description}->{data} +=
 
 351           ( $entry->trans_type->direction eq 'out' ? -$entry->qty : $entry->qty );
 
 353   if ( $last_partid > 0 && ( $allrows || ($row_ind >= $first_nr && $row_ind < $last_nr ))) {
 
 354       $self->make_row_result($last_row,$days,$last_partid);
 
 355       $report->add_data($last_row);
 
 358   my $num_rows = @{ $report->{data} } ;
 
 359   #$main::lxdebug->message(LXDebug->DEBUG2(), "count=".$row_ind." rows=".$num_rows);
 
 362       $pages->{max}  = SL::DB::Helper::Paginated::ceil($row_ind, $pages->{per_page}) || 1;
 
 363       $pages->{page} = $page < 1 ? 1: $page > $pages->{max} ? $pages->{max}: $page;
 
 364       $pages->{common} = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{page}, $pages->{max}) } ];
 
 365       $self->{pages} = $pages;
 
 366       $searchparams{action} = "usage";
 
 367       $self->{base_url} = $self->url_for(\%searchparams );
 
 368       #$main::lxdebug->message(LXDebug->DEBUG2(), "page=".$pages->{page}." url=".$self->{base_url});
 
 370       $report->set_options('raw_bottom_info_text' => $self->render('inventory/report_bottom', { output => 0 }) );
 
 372   $report->generate_with_headers();
 
 374   $main::lxdebug->leave_sub();
 
 378 sub make_row_result {
 
 379   my ($self,$row,$days,$partid) = @_;
 
 380   my $form     = $main::form;
 
 381   my $myconfig = \%main::myconfig;
 
 383   $row->{insum}->{data}  = $row->{stock}->{data} + $row->{incorrection}->{data} + $row->{found}->{data};
 
 384   $row->{outsum}->{data} = $row->{back}->{data} + $row->{outcorrection}->{data} + $row->{disposed}->{data} +
 
 385        $row->{missing}->{data} + $row->{shipped}->{data} + $row->{used}->{data};
 
 386   $row->{consumed}->{data} = $row->{outsum}->{data} -
 
 387        $row->{outcorrection}->{data} - $row->{incorrection}->{data};
 
 388   $row->{averconsumed}->{data} = $row->{consumed}->{data}*30/$days ;
 
 389   map { $row->{$_}->{data} = $form->format_amount($myconfig,$row->{$_}->{data},2); } $self->getnumcolumns();
 
 390   $row->{partnumber}->{link} = 'controller.pl?action=Part/edit&part.id' . $partid;
 
 397   my $qty = $::form->parse_amount(\%::myconfig, $::form->{qty});
 
 399     $transfer_error = t8('Cannot stock without amount');
 
 401     $transfer_error = t8('Cannot stock negative amounts');
 
 404     $::form->throw_on_error(sub {
 
 407           parts         => $self->part,
 
 408           dst_bin       => $self->bin,
 
 409           dst_wh        => $self->warehouse,
 
 412           transfer_type => 'stock',
 
 413           transfer_type_id => $::form->{transfer_type_id},
 
 414           chargenumber  => $::form->{chargenumber},
 
 415           bestbefore    => $::form->{bestbefore},
 
 416           comment       => $::form->{comment},
 
 419       } or do { $transfer_error = $EVAL_ERROR->error; }
 
 422     if (!$transfer_error) {
 
 423       if ($::form->{write_default_bin}) {
 
 424         $self->part->load;   # onhand is calculated in between. don't mess that up
 
 425         $self->part->bin($self->bin);
 
 426         $self->part->warehouse($self->warehouse);
 
 430       flash_later('info', t8('Transfer successful'));
 
 434   my %additional_redirect_params = ();
 
 435   if ($transfer_error) {
 
 436     flash_later('error', $transfer_error);
 
 437     $additional_redirect_params{$_}  = $::form->{$_} for qw(qty chargenumber bestbefore ean comment);
 
 438     $additional_redirect_params{qty} = $qty;
 
 443     action       => 'stock_in',
 
 444     part_id      => $self->part->id,
 
 445     bin_id       => $self->bin->id,
 
 446     warehouse_id => $self->warehouse->id,
 
 447     unit_id      => $self->unit->id,
 
 448     %additional_redirect_params,
 
 452 sub action_part_changed {
 
 455   # no standard? ask user if he wants to write it
 
 456   if ($self->part->id && !$self->part->bin_id && !$self->part->warehouse_id) {
 
 457     $self->js->show('#write_default_bin_span');
 
 459     $self->js->hide('#write_default_bin_span')
 
 460              ->removeAttr('#write_default_bin', 'checked');
 
 464     ->replaceWith('#warehouse_id', $self->build_warehouse_select)
 
 465     ->replaceWith('#bin_id', $self->build_bin_select)
 
 466     ->replaceWith('#unit_id', $self->build_unit_select)
 
 467     ->focus('#warehouse_id')
 
 471 sub action_warehouse_changed {
 
 475     ->replaceWith('#bin_id', $self->build_bin_select)
 
 480 sub action_mini_stock {
 
 484     ->html('#stock', $self->render('inventory/_stock', { output => 0 }))
 
 488 sub action_stocktaking {
 
 491   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Inventory);
 
 492   $::request->layout->focus('#part_id_name');
 
 493   $self->setup_stock_stocktaking_action_bar;
 
 494   $self->render('inventory/stocktaking/form', title => t8('Stocktaking'));
 
 497 sub action_save_stocktaking {
 
 500   return $self->js->flash('error', t8('Please choose a part.'))->render()
 
 501     if !$::form->{part_id};
 
 503   return $self->js->flash('error', t8('A target quantitiy has to be given'))->render()
 
 504     if $::form->{target_qty} eq '';
 
 506   my $target_qty = $::form->parse_amount(\%::myconfig, $::form->{target_qty});
 
 508   return $self->js->flash('error', t8('Error: A negative target quantity is not allowed.'))->render()
 
 511   my $stocked_qty  = _get_stocked_qty($self->part,
 
 512                                       warehouse_id => $self->warehouse->id,
 
 513                                       bin_id       => $self->bin->id,
 
 514                                       chargenumber => $::form->{chargenumber},
 
 515                                       bestbefore   => $::form->{bestbefore},);
 
 517   my $stocked_qty_in_form_units = $self->part->unit_obj->convert_to($stocked_qty, $self->unit);
 
 519   if (!$::form->{dont_check_already_counted}) {
 
 520     my $already_counted = _already_counted($self->part,
 
 521                                            warehouse_id => $self->warehouse->id,
 
 522                                            bin_id       => $self->bin->id,
 
 523                                            cutoff_date  => $::form->{cutoff_date_as_date},
 
 524                                            chargenumber => $::form->{chargenumber},
 
 525                                            bestbefore   => $::form->{bestbefore});
 
 526     if (scalar @$already_counted) {
 
 527       my $reply = $self->js->dialog->open({
 
 528         html   => $self->render('inventory/stocktaking/_already_counted_dialog',
 
 530                                 already_counted           => $already_counted,
 
 531                                 stocked_qty               => $stocked_qty,
 
 532                                 stocked_qty_in_form_units => $stocked_qty_in_form_units),
 
 533         id     => 'already_counted_dialog',
 
 535           title => t8('Already counted'),
 
 543   # - target_qty is in units given in form ($self->unit)
 
 544   # - WH->transfer expects qtys in given unit (here: unit from form (unit -> $self->unit))
 
 545   # Therefore use stocked_qty in form units for calculation.
 
 546   my $qty        = $target_qty - $stocked_qty_in_form_units;
 
 547   my $src_or_dst = $qty < 0? 'src' : 'dst';
 
 552   $::form->throw_on_error(sub {
 
 555         parts                   => $self->part,
 
 556         $src_or_dst.'_bin'      => $self->bin,
 
 557         $src_or_dst.'_wh'       => $self->warehouse,
 
 560         transfer_type           => 'stocktaking',
 
 561         chargenumber            => $::form->{chargenumber},
 
 562         bestbefore              => $::form->{bestbefore},
 
 563         ean                     => $::form->{ean},
 
 564         comment                 => $::form->{comment},
 
 565         record_stocktaking      => 1,
 
 566         stocktaking_qty         => $target_qty,
 
 567         stocktaking_cutoff_date => $::form->{cutoff_date_as_date},
 
 570     } or do { $transfer_error = $EVAL_ERROR->error; }
 
 573   return $self->js->flash('error', $transfer_error)->render()
 
 576   flash_later('info', $::locale->text('Part successful counted'));
 
 577   $self->redirect_to(action              => 'stocktaking',
 
 578                      warehouse_id        => $self->warehouse->id,
 
 579                      bin_id              => $self->bin->id,
 
 580                      cutoff_date_as_date => $self->stocktaking_cutoff_date->to_kivitendo);
 
 583 sub action_reload_stocktaking_history {
 
 586   $::form->{filter}{'cutoff_date:date'} = $self->stocktaking_cutoff_date->to_kivitendo;
 
 587   $::form->{filter}{'employee_id'}      = SL::DB::Manager::Employee->current->id;
 
 589   $self->prepare_stocktaking_report;
 
 590   $self->report_generator_list_objects(report => $self->{report}, objects => $self->stocktaking_models->get, layout => 0, header => 0);
 
 593 sub action_stocktaking_part_changed {
 
 597     ->replaceWith('#unit_id', $self->build_unit_select)
 
 598     ->focus('#target_qty')
 
 602 sub action_stocktaking_journal {
 
 605   $self->prepare_stocktaking_report(full => 1);
 
 606   $self->report_generator_list_objects(report => $self->{report}, objects => $self->stocktaking_models->get);
 
 609 sub action_stocktaking_get_warn_qty_threshold {
 
 612   return $_[0]->render(\ !!0, { type => 'text' }) if !$::form->{part_id};
 
 613   return $_[0]->render(\ !!0, { type => 'text' }) if $::form->{target_qty} eq '';
 
 614   return $_[0]->render(\ !!0, { type => 'text' }) if 0 == $::instance_conf->get_stocktaking_qty_threshold;
 
 616   my $target_qty  = $::form->parse_amount(\%::myconfig, $::form->{target_qty});
 
 617   my $stocked_qty = _get_stocked_qty($self->part,
 
 618                                      warehouse_id => $self->warehouse->id,
 
 619                                      bin_id       => $self->bin->id,
 
 620                                      chargenumber => $::form->{chargenumber},
 
 621                                      bestbefore   => $::form->{bestbefore},);
 
 622   my $stocked_qty_in_form_units = $self->part->unit_obj->convert_to($stocked_qty, $self->unit);
 
 623   my $qty        = $target_qty - $stocked_qty_in_form_units;
 
 627   if ($qty > $::instance_conf->get_stocktaking_qty_threshold) {
 
 628     $warn  = t8('The target quantity of #1 differs more than the threshold quantity of #2.',
 
 629                 $::form->{target_qty} . " " . $self->unit->name,
 
 630                 $::form->format_amount(\%::myconfig, $::instance_conf->get_stocktaking_qty_threshold, 2));
 
 632     $warn .= t8('Choose "continue" if you want to use this value. Choose "cancel" otherwise.');
 
 634   return $_[0]->render(\ $warn, { type => 'text' });
 
 637 #================================================================
 
 640   $main::auth->assert('warehouse_management');
 
 643 sub _check_warehouses {
 
 644   $_[0]->show_no_warehouses_error if !@{ $_[0]->warehouses };
 
 647 sub init_warehouses {
 
 648   SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]]);
 
 652 #  SL::DB::Manager::Bin->get_all();
 
 656   SL::DB::Manager::Unit->get_all;
 
 659 sub init_is_stocktaking {
 
 660   return $_[0]->action_name =~ m{stocktaking};
 
 663 sub init_stocktaking_models {
 
 666   SL::Controller::Helper::GetModels->new(
 
 668     model        => 'Stocktaking',
 
 674       itime        => t8('Insert Date'),
 
 675       qty          => t8('Target Qty'),
 
 676       chargenumber => t8('Charge Number'),
 
 677       comment      => t8('Comment'),
 
 678       employee     => t8('Employee'),
 
 680       partnumber   => t8('Part Number'),
 
 681       part         => t8('Part Description'),
 
 683       cutoff_date  => t8('Cutoff Date'),
 
 685     with_objects => ['employee', 'parts', 'warehouse', 'bin'],
 
 689 sub init_stocktaking_cutoff_date {
 
 692   return DateTime->from_kivitendo($::form->{cutoff_date_as_date}) if $::form->{cutoff_date_as_date};
 
 693   return SL::DB::Default->get->stocktaking_cutoff_date if SL::DB::Default->get->stocktaking_cutoff_date;
 
 695   # Default cutoff date is last day of current year, but if current month
 
 696   # is janurary, it is the last day of the last year.
 
 697   my $now    = DateTime->now_local;
 
 698   my $cutoff = DateTime->new(year => $now->year, month => 12, day => 31);
 
 699   if ($now->month < 1) {
 
 700     $cutoff->substract(years => 1);
 
 705 sub set_target_from_part {
 
 708   return if !$self->part;
 
 710   $self->warehouse($self->part->warehouse) if $self->part->warehouse;
 
 711   $self->bin(      $self->part->bin)       if $self->part->bin;
 
 714 sub sanitize_target {
 
 717   $self->warehouse($self->warehouses->[0])       if !$self->warehouse || !$self->warehouse->id;
 
 718   $self->bin      ($self->warehouse->bins->[0])  if !$self->bin       || !$self->bin->id;
 
 719 #  foreach my $warehouse ( $self->warehouses ) {
 
 720 #      $warehouse->{BINS} = [];
 
 721 #      foreach my $bin ( $self->bins ) {
 
 722 #         if ( $bin->warehouse_id == $warehouse->id ) {
 
 723 #             push @{ $warehouse->{BINS} }, $bin;
 
 729 sub load_part_from_form {
 
 730   $_[0]->part(SL::DB::Manager::Part->find_by_or_create(id => $::form->{part_id}||undef));
 
 733 sub load_unit_from_form {
 
 734   $_[0]->unit(SL::DB::Manager::Unit->find_by_or_create(id => $::form->{unit_id}));
 
 737 sub load_wh_from_form {
 
 739   $preselected = SL::DB::Default->get->stocktaking_warehouse_id if $_[0]->is_stocktaking;
 
 741   $_[0]->warehouse(SL::DB::Manager::Warehouse->find_by_or_create(id => ($::form->{warehouse_id} || $preselected)));
 
 744 sub load_bin_from_form {
 
 746   $preselected = SL::DB::Default->get->stocktaking_bin_id if $_[0]->is_stocktaking;
 
 748   $_[0]->bin(SL::DB::Manager::Bin->find_by_or_create(id => ($::form->{bin_id} || $preselected)));
 
 752   $::request->layout->add_javascripts('client_js.js');
 
 755 sub build_warehouse_select {
 
 756   select_tag('warehouse_id', $_[0]->warehouses,
 
 757    title_key => 'description',
 
 758    default   => $_[0]->warehouse->id,
 
 759    onchange  => 'reload_bin_selection()',
 
 763 sub build_bin_select {
 
 764   select_tag('bin_id', [ $_[0]->warehouse->bins ],
 
 765     title_key => 'description',
 
 766     default   => $_[0]->bin->id,
 
 770 sub build_unit_select {
 
 772     ? select_tag('unit_id', $_[0]->part->available_units,
 
 774         default   => $_[0]->part->unit_obj->id,
 
 776     : select_tag('unit_id', $_[0]->units,
 
 784   # get last 10 transaction ids
 
 785   my $query = 'SELECT trans_id, max(itime) FROM inventory GROUP BY trans_id ORDER BY max(itime) DESC LIMIT 10';
 
 786   my @ids = selectall_array_query($::form, $::form->get_standard_dbh, $query);
 
 789   $objs = SL::DB::Manager::Inventory->get_all(query => [ trans_id => \@ids ]) if @ids;
 
 791   # at most 2 of them belong to a transaction and the qty determins in or out.
 
 792   # sort them for display
 
 795     $transactions{ $_->trans_id }{ $_->qty > 0 ? 'in' : 'out' } = $_;
 
 796     $transactions{ $_->trans_id }{base} = $_;
 
 798   # and get them into order again
 
 799   my @sorted = map { $transactions{$_} } @ids;
 
 807   my $stock             = $self->part->get_simple_stock;
 
 808   $self->{stock_by_bin} = { map { $_->{bin_id} => $_ } @$stock };
 
 809   $self->{stock_empty}  = ! grep { $_->{sum} * 1 } @$stock;
 
 812 sub show_no_warehouses_error {
 
 815   my $msg = t8('No warehouse has been created yet or the quantity of the bins is not configured yet.') . ' ';
 
 817   if ($::auth->check_right($::myconfig{login}, 'config')) { # TODO wut?
 
 818     $msg .= t8('You can create warehouses and bins via the menu "System -> Warehouses".');
 
 820     $msg .= t8('Please ask your administrator to create warehouses and bins.');
 
 822   $::form->show_generic_error($msg);
 
 825 sub prepare_stocktaking_report {
 
 826   my ($self, %params) = @_;
 
 828   my $callback    = $self->stocktaking_models->get_callback;
 
 830   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
 
 831   $self->{report} = $report;
 
 833   my @columns     = qw(itime employee ean partnumber part qty unit bin chargenumber comment cutoff_date);
 
 834   my @sortable    = qw(itime employee ean partnumber part qty bin chargenumber comment cutoff_date);
 
 837     itime           => { sub   => sub { $_[0]->itime_as_timestamp },
 
 838                          text  => t8('Insert Date'), },
 
 839     employee        => { sub   => sub { $_[0]->employee->safe_name },
 
 840                          text  => t8('Employee'), },
 
 841     ean             => { sub   => sub { $_[0]->part->ean },
 
 842                          text  => t8('EAN'), },
 
 843     partnumber      => { sub   => sub { $_[0]->part->partnumber },
 
 844                          text  => t8('Part Number'), },
 
 845     part            => { sub   => sub { $_[0]->part->description },
 
 846                          text  => t8('Part Description'), },
 
 847     qty             => { sub   => sub { $_[0]->qty_as_number },
 
 848                          text  => t8('Target Qty'),
 
 850     unit            => { sub   => sub { $_[0]->part->unit },
 
 851                          text  => t8('Unit'), },
 
 852     bin             => { sub   => sub { $_[0]->bin->full_description },
 
 853                          text  => t8('Bin'), },
 
 854     chargenumber    => { text  => t8('Charge Number'), },
 
 855     comment         => { text  => t8('Comment'), },
 
 856     cutoff_date     => { sub   => sub { $_[0]->cutoff_date_as_date },
 
 857                          text  => t8('Cutoff Date'), },
 
 860   $report->set_options(
 
 861     std_column_visibility => 1,
 
 862     controller_class      => 'Inventory',
 
 863     output_format         => 'HTML',
 
 864     title                 => (!!$params{full})? $::locale->text('Stocktaking Journal') : $::locale->text('Stocktaking History'),
 
 865     allow_pdf_export      => !!$params{full},
 
 866     allow_csv_export      => !!$params{full},
 
 868   $report->set_columns(%column_defs);
 
 869   $report->set_column_order(@columns);
 
 870   $report->set_export_options(qw(stocktaking_journal filter));
 
 871   $report->set_options_from_form;
 
 872   $self->stocktaking_models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
 
 873   $self->stocktaking_models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable) if !!$params{full};
 
 874   if (!!$params{full}) {
 
 875     $report->set_options(
 
 876       raw_top_info_text    => $self->render('inventory/stocktaking/full_report_top', { output => 0 }),
 
 879   $report->set_options(
 
 880     raw_bottom_info_text => $self->render('inventory/stocktaking/report_bottom',   { output => 0 }),
 
 884 sub _get_stocked_qty {
 
 885   my ($part, %params) = @_;
 
 887   my $bestbefore_filter  = '';
 
 888   my $bestbefore_val_cnt = 0;
 
 889   if ($::instance_conf->get_show_bestbefore) {
 
 890     $bestbefore_filter  = ($params{bestbefore}) ? 'AND bestbefore = ?' : 'AND bestbefore IS NULL';
 
 891     $bestbefore_val_cnt = ($params{bestbefore}) ? 1                    : 0;
 
 895     SELECT sum(qty) FROM inventory
 
 896       WHERE parts_id = ? AND warehouse_id = ? AND bin_id = ? AND chargenumber = ? $bestbefore_filter
 
 897       GROUP BY warehouse_id, bin_id, chargenumber
 
 900   my @values = ($part->id,
 
 901                 $params{warehouse_id},
 
 903                 $params{chargenumber});
 
 904   push @values, $params{bestbefore} if $bestbefore_val_cnt;
 
 906   my ($stocked_qty) = selectrow_query($::form, $::form->get_standard_dbh, $query, @values);
 
 908   return 1*($stocked_qty || 0);
 
 911 sub _already_counted {
 
 912   my ($part, %params) = @_;
 
 914   my %bestbefore_filter;
 
 915   if ($::instance_conf->get_show_bestbefore) {
 
 916     %bestbefore_filter = (bestbefore => $params{bestbefore});
 
 919   SL::DB::Manager::Stocktaking->get_all(query => [and => [parts_id     => $part->id,
 
 920                                                           warehouse_id => $params{warehouse_id},
 
 921                                                           bin_id       => $params{bin_id},
 
 922                                                           cutoff_date  => $params{cutoff_date},
 
 923                                                           chargenumber => $params{chargenumber},
 
 924                                                           %bestbefore_filter]],
 
 925                                         sort_by => ['itime DESC']);
 
 928 sub setup_stock_in_action_bar {
 
 929   my ($self, %params) = @_;
 
 931   for my $bar ($::request->layout->get('actionbar')) {
 
 935         submit    => [ '#form', { action => 'Inventory/stock' } ],
 
 936         checks    => [ 'check_part_selection_before_stocking' ],
 
 937         accesskey => 'enter',
 
 943 sub setup_stock_usage_action_bar {
 
 944   my ($self, %params) = @_;
 
 946   for my $bar ($::request->layout->get('actionbar')) {
 
 950         submit    => [ '#form', { action => 'Inventory/usage' } ],
 
 951         accesskey => 'enter',
 
 957 sub setup_stock_stocktaking_action_bar {
 
 958   my ($self, %params) = @_;
 
 960   for my $bar ($::request->layout->get('actionbar')) {
 
 964         checks    => [ 'kivi.Inventory.check_stocktaking_qty_threshold' ],
 
 965         call      => [ 'kivi.Inventory.save_stocktaking' ],
 
 966         accesskey => 'enter',
 
 979 SL::Controller::Inventory - Controller for inventory
 
 983 This controller handles stock in, stocktaking and reports about inventory
 
 990 - warehouse withdrawal
 
 996 Stocktaking allows to document the counted quantities of parts during
 
 997 stocktaking for a certain cutoff date. Differences between counted and stocked
 
 998 quantities are corrected in the stock. The transfer type 'stocktacking' is set
 
1001 After picking a part, the mini stock for this part is displayed. At the bottom
 
1002 of the form a history of already counted parts for the current employee and the
 
1003 choosen cutoff date is shown.
 
1005 Warehouse, bin and cutoff date canbe preselected in the client configuration.
 
1007 If a part was already counted for this cutoff date, warehouse and bin, a warning
 
1008 is displayed, allowing the user to choose to add the counted quantity to the
 
1009 stocked one or to take his counted quantity as the new stocked quantity.
 
1011 There is also a journal of stocktakings.
 
1013 Templates are located under C<templates/webpages/inventory/stocktaking>.
 
1014 JavaScript functions can be found in C<js/kivi.Inventory.js>.
 
1020 =item C<action_stock_usage>
 
1022 Create a search form for stock withdrawal.
 
1023 The search parameter for report are made like the reports in bin/mozilla/rp.pl
 
1025 =item C<action_usage>
 
1027 Make a report about stock withdrawal.
 
1029 The manual pagination is implemented like the pagination in SL::Controller::CsvImport.
 
1031 =item C<action_stocktaking>
 
1033 This action renders the input form for stocktaking.
 
1035 =item C<action_save_stocktaking>
 
1037 This action saves the stocktaking values and corrects the stock after checking
 
1038 if the part is already counted for this warehouse, bin and cutoff date.
 
1039 For saving SL::WH->transfer is called.
 
1041 =item C<action_reload_stocktaking_history>
 
1043 This action is responsible for displaying the stocktaking history at the bottom
 
1044 of the form. It uses the stocktaking journal with fixed filters for cutoff date
 
1045 and the current employee. The history is displayed via javascript.
 
1047 =item C<action_stocktaking_part_changed>
 
1049 This action is called after the user selected or changed the part.
 
1051 =item C<action_stocktaking_get_warn_qty_threshold>
 
1053 This action checks if a warning should be shown and returns the warning text via
 
1054 ajax. The warning will be shown if the given target value is greater than the
 
1055 threshold given in the client configuration.
 
1057 =item C<is_stocktaking>
 
1059 This is a method to check if actions are called from stocktaking form.
 
1060 This actions should contain "stocktaking" in their name.
 
1064 =head1 SPECIAL CASES
 
1066 Because of the PFD-Table Formatter some parameters for PDF must be different to the HTML parameters.
 
1067 So in german language there are some tries to use a HTML Break in the second heading line
 
1068 to produce two line heading inside table. The actual version has some abbreviations for the header texts.
 
1072 The PDF-Table library has some limits (doesn't display all if the line is to large) so
 
1073 the format is adapted to this
 
1080 =item only for C<action_stock_usage> and C<action_usage>:
 
1082 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
 
1084 =item for stocktaking:
 
1086 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>